<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
    <id>https://pipo.blog/</id>
    <title>TechBlog by Pipo</title>
    <updated>2024-08-13T08:48:13.715Z</updated>
    <generator>https://github.com/nuxt-community/feed-module</generator>
    <author>
        <name>Onlime GmbH</name>
        <email>blog@onlime.ch</email>
        <uri>https://pipo.blog/</uri>
    </author>
    <link rel="alternate" href="https://pipo.blog/"/>
    <link rel="self" href="https://pipo.blog/feed.xml"/>
    <subtitle>Just another tech blog</subtitle>
    <entry>
        <title type="html"><![CDATA[Vue3-Toastify in a Laravel/Inertia/Vue project]]></title>
        <id>https://pipo.blog/articles/20230404-vue3-toastify-inertia</id>
        <link href="https://pipo.blog/articles/20230404-vue3-toastify-inertia"/>
        <updated>2023-04-04T00:35:00.000Z</updated>
        <summary type="html"><![CDATA[Integrate vue3-toastify into VILT (Vue3, Inertia.js, Laravel, Tailwind) stack.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/vue3-toastify-inertia.png"></p>
                      <p>Need a full-featured toast notification component for your next Vue.js project? Don't look any further: <strong><a href="https://github.com/jerrywu001/vue3-toastify" rel="nofollow noopener noreferrer" target="_blank">Vue3-Toastify</a> beats it all!</strong></p>
<p>Previously I have tried <a href="https://vue-toastification.maronato.dev/" rel="nofollow noopener noreferrer" target="_blank">Vue Toastification</a> («Light, easy and beautiful toasts») and <a href="https://szboynono.github.io/mosha-vue-toastify/" rel="nofollow noopener noreferrer" target="_blank">Mosha Vue Toastify</a> («A light weight and fun Vue 3 toast or notification or snack bar») but they are both kind of outdated and abandoned projects with latest releases dating back to prehistoric 2021. Let me show you how to integrate great looking and easy to use toast notifications into your VILT stack!</p>
<!--more-->
<p>I first started to build my own toast component but soon ended up struggling with challenging states and animations as those were my (minimal) requirements:</p>
<ul>
<li>Support multiple toasts</li>
<li>Autoclose with progress bar (success)</li>
<li>No auto-close on warning/error</li>
<li>Styled for info/success/warning/error notifications, playing nicely with Tailwind</li>
<li>Close on click</li>
<li>Pause on hover</li>
<li>Newest on top</li>
<li>Bounce transition</li>
</ul>
<p><a href="https://github.com/jerrywu001/vue3-toastify" rel="nofollow noopener noreferrer" target="_blank">Vue3-Toastify</a> offers it all, built-in, fully customizable, and «beautiful by default»! The <a href="https://github.com/jerrywu001" rel="nofollow noopener noreferrer" target="_blank">author</a> is not lying – look at this great <a href="https://vue3-toastify.js-bridge.com/get-started/introduction.html" rel="nofollow noopener noreferrer" target="_blank">feature set</a> and the <a href="https://vue3-toastify.js-bridge.com/" rel="nofollow noopener noreferrer" target="_blank">demo</a>. Even the documentation couldn't be more detailed and simply crystal clear!</p>
<blockquote>
<p>But hey, it's just a toast! Does it deserve its own blogpost? Definitely! I am a huge fanboy of that component, how well it is designed and how easy it is to use. 👏</p>
</blockquote>
<h2 id="integrate-vue3-toastify-into-vilt"><a href="#integrate-vue3-toastify-into-vilt" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Integrate Vue3-Toastify into VILT</h2>
<p>Let me now show you how to integrate Vue3-Toastify into a project built on VILT (<a href="https://vuejs.org/" rel="nofollow noopener noreferrer" target="_blank">Vue3</a>, <a href="https://inertiajs.com/" rel="nofollow noopener noreferrer" target="_blank">Inertia.js</a>, <a href="https://laravel.com/" rel="nofollow noopener noreferrer" target="_blank">Laravel</a>, <a href="https://tailwindcss.com/" rel="nofollow noopener noreferrer" target="_blank">Tailwind</a>) stack, bootstrapped with <a href="https://jetstream.laravel.com" rel="nofollow noopener noreferrer" target="_blank">Jetstream</a>.</p>
<p>Our goal is to fire flash messages from the backend, potentially more than one per request. The flash messages should be displayed in our frontend and stay open even when navigating to other pages. They should be easy closable, and the success messages should auto-close after some seconds.</p>
<p>Let's start by importing the CSS and loading the Vue3Toastify component in <code>resources/js/app.js</code> (stripped down to the bare metal):</p>
<div class="nuxt-content-highlight"><span class="filename">app.js</span><pre class="line-numbers language-js"><code><span class="token keyword module">import</span> <span class="token string">'./bootstrap'</span>
<span class="token keyword module">import</span> <span class="token string">'vue3-toastify/dist/index.css'</span>
<span class="token keyword module">import</span> <span class="token string">'../css/app.css'</span>

<span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> createApp<span class="token punctuation">,</span> h <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'vue'</span>
<span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> createInertiaApp <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'@inertiajs/vue3'</span>
<span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> resolvePageComponent <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'laravel-vite-plugin/inertia-helpers'</span>
<span class="token keyword module">import</span> <span class="token imports"><span class="token maybe-class-name">Vue3Toastify</span></span> <span class="token keyword module">from</span> <span class="token string">'vue3-toastify'</span>

<span class="token function">createInertiaApp</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
    <span class="token function-variable function">resolve</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter">name</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token function">resolvePageComponent</span><span class="token punctuation">(</span><span class="token template-string"><span class="token template-punctuation string">`</span><span class="token string">./Pages/</span><span class="token interpolation"><span class="token interpolation-punctuation punctuation">${</span>name<span class="token interpolation-punctuation punctuation">}</span></span><span class="token string">.vue</span><span class="token template-punctuation string">`</span></span><span class="token punctuation">,</span> <span class="token keyword module">import</span><span class="token punctuation">.</span><span class="token property-access">meta</span><span class="token punctuation">.</span><span class="token method function property-access">glob</span><span class="token punctuation">(</span><span class="token string">'./Pages/**/*.vue'</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token function">setup</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> el<span class="token punctuation">,</span> <span class="token maybe-class-name">App</span><span class="token punctuation">,</span> props<span class="token punctuation">,</span> plugin <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
        <span class="token keyword control-flow">return</span> <span class="token function">createApp</span><span class="token punctuation">(</span><span class="token punctuation">{</span> <span class="token function-variable function">render</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token function">h</span><span class="token punctuation">(</span><span class="token maybe-class-name">App</span><span class="token punctuation">,</span> props<span class="token punctuation">)</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token method function property-access">use</span><span class="token punctuation">(</span>plugin<span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token method function property-access">use</span><span class="token punctuation">(</span><span class="token maybe-class-name">Vue3Toastify</span><span class="token punctuation">)</span>
            <span class="token punctuation">.</span><span class="token method function property-access">mount</span><span class="token punctuation">(</span>el<span class="token punctuation">)</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>
</code></pre></div>
<p>Import vue3-toastify's CSS before your <code>app.css</code>, so you could override some styles there, e.g.:</p>
<div class="nuxt-content-highlight"><span class="filename">app.css</span><pre class="line-numbers language-css"><code><span class="token selector"><span class="token pseudo-class">:root</span></span> <span class="token punctuation">{</span>
    <span class="token variable">--toastify-toast-width</span><span class="token punctuation">:</span> <span class="token number">400</span><span class="token unit">px</span><span class="token punctuation">;</span>
    <span class="token variable">--toastify-color-warning</span><span class="token punctuation">:</span> <span class="token hexcode color">#ec9b00</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>(see <a href="https://vue3-toastify.js-bridge.com/usage/how-to-style.html" rel="nofollow noopener noreferrer" target="_blank">How to style</a> docs for all available <code>--toastify-*</code> CSS variables)</p>
<p>You could now fire a toast message from anywhere in your JS code like this (see <a href="https://vue3-toastify.js-bridge.com/api/toast.html#usages" rel="nofollow noopener noreferrer" target="_blank">Usages</a>):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-js"><code><span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> toast <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'vue3-toastify'</span>

<span class="token keyword">const</span> options <span class="token operator">=</span> <span class="token punctuation">{</span>
  <span class="token literal-property property">autoClose</span><span class="token operator">:</span> <span class="token number">5000</span><span class="token punctuation">,</span>
  <span class="token literal-property property">type</span><span class="token operator">:</span> toast<span class="token punctuation">.</span><span class="token constant">TYPE</span><span class="token punctuation">.</span><span class="token constant">SUCCESS</span><span class="token punctuation">,</span>
  <span class="token literal-property property">newestOnTop</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
  <span class="token literal-property property">theme</span><span class="token operator">:</span> toast<span class="token punctuation">.</span><span class="token constant">THEME</span><span class="token punctuation">.</span><span class="token constant">COLORED</span><span class="token punctuation">,</span>
  <span class="token comment">// and so on ...</span>
<span class="token punctuation">}</span> <span class="token keyword module">as</span> <span class="token maybe-class-name">ToastOptions</span><span class="token punctuation">;</span>

<span class="token comment">// display toasts</span>
<span class="token keyword">const</span> toastId <span class="token operator">=</span> <span class="token function">toast</span><span class="token punctuation">(</span><span class="token string">"Hello"</span><span class="token punctuation">,</span> options <span class="token keyword module">as</span> <span class="token maybe-class-name">ToastOptions</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">//shortcut to different types</span>
toast<span class="token punctuation">.</span><span class="token method function property-access">success</span><span class="token punctuation">(</span><span class="token string">"Hello"</span><span class="token punctuation">,</span> options <span class="token keyword module">as</span> <span class="token maybe-class-name">ToastOptions</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
toast<span class="token punctuation">.</span><span class="token method function property-access">info</span><span class="token punctuation">(</span><span class="token string">"World"</span><span class="token punctuation">,</span> options <span class="token keyword module">as</span> <span class="token maybe-class-name">ToastOptions</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
toast<span class="token punctuation">.</span><span class="token method function property-access">warn</span><span class="token punctuation">(</span><span class="token maybe-class-name">MyComponent</span><span class="token punctuation">,</span> options <span class="token keyword module">as</span> <span class="token maybe-class-name">ToastOptions</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
toast<span class="token punctuation">.</span><span class="token method function property-access">error</span><span class="token punctuation">(</span><span class="token string">"Error"</span><span class="token punctuation">,</span> options <span class="token keyword module">as</span> <span class="token maybe-class-name">ToastOptions</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div>
<p>But we don't want to care about firing toasts in the frontend – just fire them whenever they pop in! Read on...</p>
<h2 id="toastnotifications-vue-component"><a href="#toastnotifications-vue-component" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>ToastNotifications Vue component</h2>
<p>Let's now write our global <code>ToastNotifications</code> Vue component. First, create a composable <code>resources/js/Composables/ToastNotifications.js</code> with all the «business logic»:</p>
<div class="nuxt-content-highlight"><span class="filename">ToastNotifications.js</span><pre class="line-numbers language-js"><code><span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> ref<span class="token punctuation">,</span> watch <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'vue'</span>
<span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> usePage <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'@inertiajs/vue3'</span>
<span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> toast <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'vue3-toastify'</span>

<span class="token keyword">const</span> toasts <span class="token operator">=</span> <span class="token function">ref</span><span class="token punctuation">(</span><span class="token punctuation">)</span>

<span class="token keyword">const</span> reToasted <span class="token operator">=</span> <span class="token function">ref</span><span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span>

<span class="token keyword">function</span> <span class="token function">fireToast</span><span class="token punctuation">(</span><span class="token parameter">notification<span class="token punctuation">,</span> sticky <span class="token operator">=</span> <span class="token boolean">false</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token function">toast</span><span class="token punctuation">(</span>notification<span class="token punctuation">.</span><span class="token property-access">message</span><span class="token punctuation">,</span> <span class="token punctuation">{</span>
        <span class="token literal-property property">toastId</span><span class="token operator">:</span> notification<span class="token punctuation">.</span><span class="token property-access">id</span><span class="token punctuation">,</span>
        <span class="token literal-property property">type</span><span class="token operator">:</span> notification<span class="token punctuation">.</span><span class="token property-access">type</span><span class="token punctuation">,</span>
        <span class="token literal-property property">newestOnTop</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token literal-property property">theme</span><span class="token operator">:</span> toast<span class="token punctuation">.</span><span class="token constant">THEME</span><span class="token punctuation">.</span><span class="token constant">COLORED</span><span class="token punctuation">,</span>
        <span class="token literal-property property">autoClose</span><span class="token operator">:</span> <span class="token operator">!</span>sticky <span class="token operator">&#x26;&#x26;</span> <span class="token punctuation">(</span>notification<span class="token punctuation">.</span><span class="token property-access">type</span> <span class="token operator">==</span> <span class="token string">'success'</span> <span class="token operator">?</span> <span class="token number">5000</span> <span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token literal-property property">closeOnClick</span><span class="token operator">:</span> <span class="token operator">!</span>sticky<span class="token punctuation">,</span>
        <span class="token function-variable function">onClose</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token punctuation">(</span>reToasted<span class="token punctuation">.</span><span class="token property-access">value</span> <span class="token operator">=</span> <span class="token boolean">false</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>

<span class="token keyword">function</span> <span class="token function">fireToasts</span><span class="token punctuation">(</span><span class="token parameter">sticky <span class="token operator">=</span> <span class="token boolean">false</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    toasts<span class="token punctuation">.</span><span class="token property-access">value</span><span class="token operator">?.</span><span class="token method function property-access">forEach</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">notification</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token function">fireToast</span><span class="token punctuation">(</span>notification<span class="token punctuation">,</span> sticky<span class="token punctuation">)</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>

<span class="token keyword">function</span> <span class="token function">toastAgain</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    reToasted<span class="token punctuation">.</span><span class="token property-access">value</span> <span class="token operator">?</span> toast<span class="token punctuation">.</span><span class="token method function property-access">remove</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">:</span> <span class="token function">fireToasts</span><span class="token punctuation">(</span><span class="token boolean">true</span><span class="token punctuation">)</span>
    reToasted<span class="token punctuation">.</span><span class="token property-access">value</span> <span class="token operator">=</span> <span class="token operator">!</span>reToasted<span class="token punctuation">.</span><span class="token property-access">value</span>
<span class="token punctuation">}</span>

<span class="token function">watch</span><span class="token punctuation">(</span>
    <span class="token comment">// NOTE: Since Inertia.js 1.0.1, usePage() may return null initially.</span>
    <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token function">usePage</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">?.</span>props<span class="token operator">?.</span>flash<span class="token punctuation">.</span><span class="token property-access">toasts</span><span class="token punctuation">,</span>
    <span class="token punctuation">(</span><span class="token parameter">newToasts</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token punctuation">{</span>
        reToasted<span class="token punctuation">.</span><span class="token property-access">value</span> <span class="token operator">=</span> <span class="token boolean">false</span>
        toasts<span class="token punctuation">.</span><span class="token property-access">value</span> <span class="token operator">=</span> newToasts
        <span class="token function">fireToasts</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">)</span>

<span class="token keyword module">export</span> <span class="token keyword">function</span> <span class="token function">useToasts</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword control-flow">return</span> <span class="token punctuation">{</span>
        toasts<span class="token punctuation">,</span>
        toastAgain<span class="token punctuation">,</span>
        reToasted<span class="token punctuation">,</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>This fires toast messages whenever the <code>flash.toasts</code> shared prop gets filled. To make sure we don't ever create duplicate toasts, let's give each toast a unique id. Whenever the watched <code>usePage().props.flash.toasts</code> changes, the new toasts get fired.</p>
<p>I am using the following helpers in <code>bootstrap/helpers.php</code> (loaded in <code>composer.json</code>'s <code>"autoload": { "files": [ "bootstrap/helpers.php" ], }</code> where the toasts are getting added to the session:</p>
<div class="nuxt-content-highlight"><span class="filename">helpers.php</span><pre class="line-numbers language-php"><code><span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span> <span class="token function">function_exists</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'toast'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">function</span> <span class="token function-definition function">toast</span><span class="token punctuation">(</span><span class="token class-name type-declaration">ToastType</span> <span class="token variable">$type</span><span class="token punctuation">,</span> <span class="token keyword type-hint">string</span> <span class="token variable">$message</span><span class="token punctuation">,</span> <span class="token operator">?</span><span class="token class-name type-declaration">RedirectResponse</span> <span class="token variable">$response</span> <span class="token operator">=</span> <span class="token constant">null</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token variable">$toasts</span> <span class="token operator">=</span> <span class="token function">session</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'toasts'</span><span class="token punctuation">,</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token variable">$toasts</span><span class="token punctuation">[</span><span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token punctuation">[</span>
            <span class="token string single-quoted-string">'id'</span>      <span class="token operator">=></span> <span class="token scope">Str<span class="token punctuation">::</span></span><span class="token function">uuid</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
            <span class="token string single-quoted-string">'type'</span>    <span class="token operator">=></span> <span class="token variable">$type</span><span class="token operator">-></span><span class="token property">value</span><span class="token punctuation">,</span>
            <span class="token string single-quoted-string">'message'</span> <span class="token operator">=></span> <span class="token variable">$message</span><span class="token punctuation">,</span>
        <span class="token punctuation">]</span><span class="token punctuation">;</span>
        <span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token variable">$response</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
            <span class="token keyword">return</span>  <span class="token variable">$response</span><span class="token operator">-></span><span class="token function">with</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'toasts'</span><span class="token punctuation">,</span> <span class="token variable">$toasts</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span> <span class="token keyword">else</span> <span class="token punctuation">{</span>
            <span class="token function">session</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">flash</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'toasts'</span><span class="token punctuation">,</span> <span class="token variable">$toasts</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span> <span class="token function">function_exists</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'toast_success'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">function</span> <span class="token function-definition function">toast_success</span><span class="token punctuation">(</span><span class="token keyword type-hint">string</span> <span class="token variable">$message</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">return</span> <span class="token function">toast</span><span class="token punctuation">(</span><span class="token scope">ToastType<span class="token punctuation">::</span></span><span class="token constant">SUCCESS</span><span class="token punctuation">,</span> <span class="token variable">$message</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span> <span class="token function">function_exists</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'toast_warning'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">function</span> <span class="token function-definition function">toast_warning</span><span class="token punctuation">(</span><span class="token keyword type-hint">string</span> <span class="token variable">$message</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">return</span> <span class="token function">toast</span><span class="token punctuation">(</span><span class="token scope">ToastType<span class="token punctuation">::</span></span><span class="token constant">WARNING</span><span class="token punctuation">,</span> <span class="token variable">$message</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token keyword">if</span> <span class="token punctuation">(</span><span class="token operator">!</span> <span class="token function">function_exists</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'toast_error'</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">function</span> <span class="token function-definition function">toast_error</span><span class="token punctuation">(</span><span class="token keyword type-hint">string</span> <span class="token variable">$message</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">return</span> <span class="token function">toast</span><span class="token punctuation">(</span><span class="token scope">ToastType<span class="token punctuation">::</span></span><span class="token constant">ERROR</span><span class="token punctuation">,</span> <span class="token variable">$message</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>... and the related PHP enum – completely unnecessary but I like to favor enums over strings wherever I can:</p>
<div class="nuxt-content-highlight"><span class="filename">ToastType.php</span><pre class="line-numbers language-php"><code><span class="token keyword">namespace</span> <span class="token package">App<span class="token punctuation">\</span>Enums</span><span class="token punctuation">;</span>

<span class="token keyword">enum</span> <span class="token class-name-definition class-name">ToastType</span><span class="token punctuation">:</span> <span class="token keyword type-declaration">string</span>
<span class="token punctuation">{</span>
    <span class="token keyword">case</span> <span class="token constant">SUCCESS</span> <span class="token operator">=</span> <span class="token string single-quoted-string">'success'</span><span class="token punctuation">;</span>
    <span class="token keyword">case</span> <span class="token constant">WARNING</span> <span class="token operator">=</span> <span class="token string single-quoted-string">'warning'</span><span class="token punctuation">;</span>
    <span class="token keyword">case</span> <span class="token constant">ERROR</span> <span class="token operator">=</span> <span class="token string single-quoted-string">'error'</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Now, to make the toasts available to your frontend, add them to the <a href="https://inertiajs.com/shared-data" rel="nofollow noopener noreferrer" target="_blank">Inertia shared data</a> in <code>HandleInertiaRequests</code> middleware:</p>
<div class="nuxt-content-highlight"><span class="filename">HandleInertiaRequests.php</span><pre class="line-numbers language-php"><code><span class="token php language-php"><span class="token delimiter important">&#x3C;?php</span>

<span class="token keyword">namespace</span> <span class="token package">App<span class="token punctuation">\</span>Http<span class="token punctuation">\</span>Middleware</span><span class="token punctuation">;</span>

<span class="token keyword">use</span> <span class="token package">Inertia<span class="token punctuation">\</span>Middleware</span><span class="token punctuation">;</span>

<span class="token keyword">class</span> <span class="token class-name-definition class-name">HandleInertiaRequests</span> <span class="token keyword">extends</span> <span class="token class-name">Middleware</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">share</span><span class="token punctuation">(</span><span class="token class-name type-declaration">Request</span> <span class="token variable">$request</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token keyword">return</span> <span class="token function">array_merge</span><span class="token punctuation">(</span><span class="token scope"><span class="token keyword">parent</span><span class="token punctuation">::</span></span><span class="token function">share</span><span class="token punctuation">(</span><span class="token variable">$request</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">[</span>
            <span class="token string single-quoted-string">'flash'</span> <span class="token operator">=></span> <span class="token keyword">fn</span> <span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token punctuation">[</span>
                <span class="token string single-quoted-string">'toasts'</span><span class="token operator">=></span> <span class="token variable">$request</span><span class="token operator">-></span><span class="token function">session</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">get</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'toasts'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
            <span class="token punctuation">]</span><span class="token punctuation">,</span>
            <span class="token comment">// ...</span>
        <span class="token punctuation">]</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</span></code></pre></div>
<p>And finally, build your <code>ToastNotifications</code> Vue component:</p>
<div class="nuxt-content-highlight"><span class="filename">ToastNotifications.vue</span><pre class="line-numbers language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>script</span> <span class="token attr-name">setup</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
<span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> <span class="token maybe-class-name">BellAlertIcon</span><span class="token punctuation">,</span> <span class="token maybe-class-name">BellSlashIcon</span> <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'@heroicons/vue/24/outline'</span>
<span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> useToasts <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'@/Composables/ToastNotifications'</span>

<span class="token keyword">const</span> <span class="token punctuation">{</span> toasts<span class="token punctuation">,</span> toastAgain<span class="token punctuation">,</span> reToasted <span class="token punctuation">}</span> <span class="token operator">=</span> <span class="token function">useToasts</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>script</span><span class="token punctuation">></span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>template</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>div</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>button</span>
            <span class="token attr-name">v-if</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>toasts<span class="token punctuation">"</span></span>
            <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>flex h-10 w-10 items-center justify-center rounded-full bg-gray-100 hover:bg-gray-800 hover:text-white dark:bg-gray-700 dark:text-gray-300 dark:hover:bg-gray-600 dark:hover:text-white<span class="token punctuation">"</span></span>
            <span class="token attr-name">:class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{ 'animate-pulse': !reToasted }<span class="token punctuation">"</span></span>
            <span class="token attr-name">@click</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>toastAgain()<span class="token punctuation">"</span></span>
        <span class="token punctuation">></span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>component</span> <span class="token attr-name">:is</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>reToasted ? 'BellSlashIcon' : 'BellAlertIcon'<span class="token punctuation">"</span></span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>h-5 w-5<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>button</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>div</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>template</span><span class="token punctuation">></span></span>
</code></pre></div>
<p>You don't need to get that fancy and could also just load the toasts somewhere else without an extra Vue component, or at least without the <code>&#x3C;template></code> part. But this component provides you the following:</p>
<ul>
<li>Shows nothing when there is no toast message</li>
<li>Shows you a nice <code>BellAlertIcon</code> button when there are any toasts available (open or already closed but still on the same page)</li>
<li>You can click on the bell-alert icon to re-open the previous messages again.</li>
<li>When toasts are re-opened, they are not auto-closed (see <code>sticky</code> flag in <code>ToastNotifications.js</code> function <code>fireToast()</code>), but for easy closing of all toasts, you can just click on the bell-slash icon instead of closing each toast message individually.</li>
</ul>
<p>Just put this component into your app header or main layout:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>template</span><span class="token punctuation">></span></span>
    <span class="token comment">&#x3C;!-- ... --></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>ToastNotifications</span> <span class="token punctuation">/></span></span>
</code></pre></div>
<p>done. 🎉</p>
<blockquote>
<p>Oh, and in case you're missing some import statements in my Vue code, I love on-demand component auto-importing with <a href="https://github.com/antfu/unplugin-vue-components" rel="nofollow noopener noreferrer" target="_blank">unplugin-vue-components</a>!</p>
</blockquote>
<h2 id="usage-in-your-controllers"><a href="#usage-in-your-controllers" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Usage in your Controllers</h2>
<p>In your backend controllers, you can now simply use:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-php"><code><span class="token function">toast_success</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'User successfully updated!'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div>
<p>Or how about extending the <code>RedirectResponse</code> in your <code>AppServiceProvider.php</code>?:</p>
<div class="nuxt-content-highlight"><span class="filename">AppServiceProvider.php</span><pre class="line-numbers language-php"><code><span class="token keyword">class</span> <span class="token class-name-definition class-name">AppServiceProvider</span> <span class="token keyword">extends</span> <span class="token class-name">ServiceProvider</span>
<span class="token punctuation">{</span>
    <span class="token comment">// ...</span>
    <span class="token keyword">private</span> <span class="token keyword">function</span> <span class="token function-definition function">registerMacros</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token scope">RedirectResponse<span class="token punctuation">::</span></span><span class="token function">macro</span><span class="token punctuation">(</span>
            <span class="token string single-quoted-string">'withSuccess'</span><span class="token punctuation">,</span>
            <span class="token keyword">fn</span> <span class="token punctuation">(</span><span class="token keyword type-hint">string</span> <span class="token variable">$message</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">toast</span><span class="token punctuation">(</span><span class="token scope">ToastType<span class="token punctuation">::</span></span><span class="token constant">SUCCESS</span><span class="token punctuation">,</span> <span class="token variable">$message</span><span class="token punctuation">,</span> <span class="token this keyword">$this</span><span class="token punctuation">)</span>
        <span class="token punctuation">)</span><span class="token punctuation">;</span>

        <span class="token scope">RedirectResponse<span class="token punctuation">::</span></span><span class="token function">macro</span><span class="token punctuation">(</span>
            <span class="token string single-quoted-string">'withError'</span><span class="token punctuation">,</span>
            <span class="token keyword">fn</span> <span class="token punctuation">(</span><span class="token keyword type-hint">string</span> <span class="token variable">$message</span><span class="token punctuation">)</span> <span class="token operator">=></span> <span class="token function">toast</span><span class="token punctuation">(</span><span class="token scope">ToastType<span class="token punctuation">::</span></span><span class="token constant">ERROR</span><span class="token punctuation">,</span> <span class="token variable">$message</span><span class="token punctuation">,</span> <span class="token this keyword">$this</span><span class="token punctuation">)</span>
        <span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
</code></pre></div>
<p>Like this, you could just append the message to the redirect response:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-php"><code>    <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">update</span><span class="token punctuation">(</span><span class="token class-name type-declaration">UpdateCustomerRequest</span> <span class="token variable">$request</span><span class="token punctuation">,</span> <span class="token class-name type-declaration">Customer</span> <span class="token variable">$customer</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token variable">$customer</span><span class="token operator">-></span><span class="token function">update</span><span class="token punctuation">(</span><span class="token variable">$request</span><span class="token operator">-></span><span class="token function">validated</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token keyword">return</span> <span class="token function">back</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">withSuccess</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'The customer was fully pimped.'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
</code></pre></div>
<p>I wish you a perfectly toasted 🍞 slice of bread that never falls on the wrong side! If it does, just put a <code>!</code> in front.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="frontend" term="frontend"/>
        <category label="backend" term="backend"/>
        <category label="vue.js" term="vue.js"/>
        <category label="laravel" term="laravel"/>
        <category label="inertia.js" term="inertia.js"/>
        <category label="js" term="js"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Vue.js 3 v-model two-way data binding for Dummies]]></title>
        <id>https://pipo.blog/articles/20220714-vue3-v-model-binding</id>
        <link href="https://pipo.blog/articles/20220714-vue3-v-model-binding"/>
        <updated>2022-07-14T12:00:00.000Z</updated>
        <summary type="html"><![CDATA[How to make your custom Vue 3 form components v-model capable.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/vue3-v-model-binding.png"></p>
                      <p>Vue <code>v-model</code> is a directive that creates a two-way data binding between a value in our template and a value in our data properties, while <code>v-bind</code> only binds data one way. A common use case for using v-model is when designing forms and inputs.</p>
<p>While the core concept of <code>v-model</code> is quite straightforward, you might struggle with it when building advanced custom form components. Personally, this was my main pain point when learning Vue, especially since there are so many different ways this can be handled and tutorials diverge quite a bit.</p>
<p>Vue's official <a href="https://vuejs.org/guide/components/events.html#usage-with-v-model" rel="nofollow noopener noreferrer" target="_blank">Components In-Depth > Events > Usage with <code>v-model</code></a> documentation only covers the most basic usage without providing any advanced examples. So I put together some use cases for you to learn more about <code>v-model</code>. The main question is: <strong>How can I make that custom component reusable so that it can be used as a form component with two-way data binding?</strong></p>
<!--more-->
<h2 id="v-model-directive-explained"><a href="#v-model-directive-explained" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a><code>v-model</code> directive explained</h2>
<p>Vue's <a href="https://vuejs.org/api/built-in-directives.html#v-model" rel="nofollow noopener noreferrer" target="_blank"><code>v-model</code> directive</a> does the following under the hood:</p>
<div class="nuxt-content-highlight"><pre class="language-html line-numbers"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>MyChild</span> <span class="token attr-name">v-model</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>someRef<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
<span class="token comment">&#x3C;!-- same as --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>MyChild</span>
    <span class="token attr-name">:modelValue</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>someRef<span class="token punctuation">"</span></span>
    <span class="token attr-name"><span class="token namespace">@update:</span>modelValue</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>someRef = $event<span class="token punctuation">"</span></span>
<span class="token punctuation">/></span></span>
</code></pre></div>
<p>By default, the following applies to <code>v-model</code>:</p>
<ul>
<li>default <strong>prop</strong> name is <code>modelValue</code></li>
<li>default <strong>event</strong> is <code>update:modelValue</code></li>
</ul>
<p>This prop and event name will then be available in your child component.</p>
<blockquote>
<p>The default prop and event name could be changed by appending the name with a colon: <code>v-model:name</code>, but let's stick with the default! Changing the name would only be needed if you would like to assign multiple independent <code>v-model</code> directives on the same component tag, which is very rarely used.</p>
</blockquote>
<p>So, in every custom component which should support two-way data binding you need to implement the following:</p>
<ul>
<li>declare <code>modelValue</code> as a prop</li>
<li>emit <code>update:modelValue</code> event when the input changes</li>
</ul>
<p>Let's dive into a selection of 3 use cases which all use the recommended Vue 3 Composition API <a href="https://vuejs.org/api/sfc-script-setup.html" rel="nofollow noopener noreferrer" target="_blank"><code>&#x3C;script setup></code></a> syntax.</p>
<h2 id="custominput-form-component"><a href="#custominput-form-component" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a><code>CustomInput</code> form component</h2>
<p>In this example, we are building a <code>CustomInput</code> form component that wraps the native <a href="https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input" rel="nofollow noopener noreferrer" target="_blank"><code>&#x3C;input></code> HTML element</a>. It could then be used like this from our parent component:</p>
<div class="nuxt-content-highlight"><pre class="language-html line-numbers"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>script</span> <span class="token attr-name">setup</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
<span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> ref <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'vue'</span>
<span class="token keyword module">import</span> <span class="token imports"><span class="token maybe-class-name">CustomInput</span></span> <span class="token keyword module">from</span> <span class="token string">'./CustomInput.vue'</span>

<span class="token keyword">const</span> fullname <span class="token operator">=</span> <span class="token function">ref</span><span class="token punctuation">(</span><span class="token string">''</span><span class="token punctuation">)</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>script</span><span class="token punctuation">></span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>template</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>form</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>CustomInput</span> <span class="token attr-name">v-model</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>fullname<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>form</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>template</span><span class="token punctuation">></span></span>
</code></pre></div>
<p>Our child component (basic <code>CustomInput</code> form component) looks like this:</p>
<div class="nuxt-content-highlight"><pre class="language-html line-numbers"><code><span class="token comment">&#x3C;!-- CustomInput.vue --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>script</span> <span class="token attr-name">setup</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
<span class="token function">defineProps</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string">'modelValue'</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
<span class="token function">defineEmits</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string">'update:modelValue'</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>script</span><span class="token punctuation">></span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>template</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>input</span>
        <span class="token attr-name">:value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>modelValue<span class="token punctuation">"</span></span>
        <span class="token attr-name">@input</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$emit('update:modelValue', $event.target.value)<span class="token punctuation">"</span></span>
    <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>template</span><span class="token punctuation">></span></span>
</code></pre></div>
<p>In this example, the data we are binding to our event is <code>$event.target.value</code> which is the actual <code>String</code> value of this input field.</p>
<p><strong>Remember:</strong> Native HTML elements always fire <a href="https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Building_blocks/Events" rel="nofollow noopener noreferrer" target="_blank">Events</a>, so you'll never get a primitive (like e.g. a <code>Boolean</code> or a <code>String</code>) as <code>$event</code>. That's why you need to retrieve the effective input element's value with <code>$event.target.value</code> in this case.</p>
<h2 id="customswitch-toggle-form-component"><a href="#customswitch-toggle-form-component" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a><code>CustomSwitch</code> (Toggle) form component</h2>
<p>Let's create a slightly more sophisticated custom <strong>Switch (Toggle)</strong> form component, using <a href="https://headlessui.com/vue/switch" rel="nofollow noopener noreferrer" target="_blank">Headless UI <code>Switch</code> component</a>. Please note that the official Headless UI documentation provides us with a basic example where they use an <code>enabled</code> ref to store the current state:</p>
<div class="nuxt-content-highlight"><pre class="language-html line-numbers"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>script</span> <span class="token attr-name">setup</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
<span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> ref <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'vue'</span>
<span class="token keyword">const</span> enabled <span class="token operator">=</span> <span class="token function">ref</span><span class="token punctuation">(</span><span class="token boolean">false</span><span class="token punctuation">)</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>script</span><span class="token punctuation">></span></span>
</code></pre></div>
<p>This extra ref is not needed inside our custom component, as the <a href="https://headlessui.com/vue/switch#switch" rel="nofollow noopener noreferrer" target="_blank"><code>Switch</code></a> component itself fires an event with a primitive <code>Boolean</code> that represents the current on/off state (see <code>Switch</code> Component API documentation, where <code>v-model</code> supports a <code>Boolean</code>). So, <code>$event</code> is simply the <code>Boolean</code> and we can pass it directly to our parent component with <code>@update:modelValue="$emit('update:modelValue', $event)"</code>.</p>
<p>Side-note: In the example below, we are going to use the newer syntax without depending on the "magic" <code>$event</code> variable, so this is exactly the same in Vue 3 (see <a href="https://vuejs.org/guide/components/events.html#usage-with-v-model" rel="nofollow noopener noreferrer" target="_blank">Component Events > Usage with <code>v-model</code></a>):</p>
<div class="nuxt-content-highlight"><pre class="language-html line-numbers"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Switch</span>
    <span class="token attr-name"><span class="token namespace">@update:</span>modelValue</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$emit('update:modelValue', $event)<span class="token punctuation">"</span></span>
<span class="token punctuation">/></span></span>
<span class="token comment">&#x3C;!-- same as --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Switch</span>
    <span class="token attr-name"><span class="token namespace">@update:</span>modelValue</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(newValue) => $emit('update:modelValue', newValue)<span class="token punctuation">"</span></span>
<span class="token punctuation">/></span></span>
</code></pre></div>
<p>So our fully working (and minimally styled) <code>CustomSwitch</code> component looks like this:</p>
<div class="nuxt-content-highlight"><pre class="language-html line-numbers"><code><span class="token comment">&#x3C;!-- CustomSwitch.vue --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>script</span> <span class="token attr-name">setup</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
<span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> <span class="token maybe-class-name">Switch</span> <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'@headlessui/vue'</span>

<span class="token function">defineProps</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
    <span class="token literal-property property">modelValue</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token known-class-name class-name">Boolean</span><span class="token punctuation">,</span>
        <span class="token keyword module">default</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token literal-property property">label</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token known-class-name class-name">String</span><span class="token punctuation">,</span>
        <span class="token keyword module">default</span><span class="token operator">:</span> <span class="token string">''</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>

<span class="token function">defineEmits</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string">'update:modelValue'</span><span class="token punctuation">]</span><span class="token punctuation">)</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>script</span><span class="token punctuation">></span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>template</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Switch</span>
        <span class="token attr-name">:class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>modelValue ? 'bg-primary-600' : 'bg-gray-300'<span class="token punctuation">"</span></span>
        <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>relative inline-flex h-6 w-11 items-center rounded-full<span class="token punctuation">"</span></span>
        <span class="token attr-name">:modelValue</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>modelValue<span class="token punctuation">"</span></span>
        <span class="token attr-name"><span class="token namespace">@update:</span>modelValue</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(newValue) => $emit('update:modelValue', newValue)<span class="token punctuation">"</span></span>
    <span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>span</span> <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>sr-only<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>{{ label }}<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>span</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>span</span>
            <span class="token attr-name">:class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>modelValue ? 'translate-x-5' : 'translate-x-0'<span class="token punctuation">"</span></span>
            <span class="token attr-name">class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>inline-block h-5 w-5 transform rounded-full bg-white<span class="token punctuation">"</span></span>
        <span class="token punctuation">/></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Switch</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>template</span><span class="token punctuation">></span></span>
</code></pre></div>
<p><strong>Slot props:</strong></p>
<p>Please note, in this example, we use the <code>modelValue</code> prop directly for the current state of the Switch/Toggle. The <a href="https://headlessui.com/vue/switch" rel="nofollow noopener noreferrer" target="_blank">Headless UI  <code>Switch</code> component</a> would offer a <code>checked</code> slot prop for that as well, but this would <strong>only</strong> be available inside the <code>Switch</code> component tag and not in e.g. any direct property of the <code>Switch</code> tag itself (e.g. <code>class</code>, where we would still depend on either <code>modelValue</code> or some other ref that represents the current state). The following could be done, but just makes our example more complex, as we would use <code>checked</code> slot prop in addition to the actual <code>modelValue</code>:</p>
<div class="nuxt-content-highlight"><pre class="language-html line-numbers"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>template</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Switch</span>
        <span class="token attr-name">v-slot</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{ checked }<span class="token punctuation">"</span></span>
        <span class="token attr-name">:class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>modelValue ? 'bg-primary-600' : 'bg-gray-300'<span class="token punctuation">"</span></span>
        <span class="token attr-name">...</span>
    <span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>span</span> <span class="token attr-name">:class</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>checked ? 'translate-x-5' : 'translate-x-0'<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Switch</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>template</span><span class="token punctuation">></span></span>
</code></pre></div>
<p><strong>Immutable props:</strong></p>
<p>And why can't we directly bind the <code>modelValue</code> to the <code>Switch</code> component using <code>v-model="modelValue"</code> inside our <code>CustomSwitch</code> component? Read this: <a href="https://vuejs.org/guide/components/props.html#one-way-data-flow" rel="nofollow noopener noreferrer" target="_blank">Props: One-Way Data Flow</a></p>
<blockquote>
<p>All props form a one-way-down binding between the child property and the parent one: when the parent property updates, it will flow down to the child, but not the other way around. This prevents child components from accidentally mutating the parent's  state, which can make your app's data flow harder to understand.
(...)
This means you should <strong>not</strong> attempt to mutate a prop inside a child component. If you do, Vue will warn you in the console</p>
</blockquote>
<p>So, <strong>never try to mutate a property</strong> in your custom component! Always work around it by only reading the prop value and firing events to pass the changed value back to the parent component, as we did in above example.</p>
<h2 id="customlistbox-select-for-component"><a href="#customlistbox-select-for-component" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a><code>CustomListbox</code> (Select) for component</h2>
<p>Let's dive into another slightly more complex example of <code>v-model</code> two-way data binding, using <a href="https://headlessui.com/vue/listbox" rel="nofollow noopener noreferrer" target="_blank">Headless UI <code>Listbox</code> component</a>. We can use this component to build custom, accessible select menus.</p>
<p>Options are passed in an object notation as <code>options</code> prop to our custom component:</p>
<div class="nuxt-content-highlight"><pre class="language-html line-numbers"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>template</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>ListboxSelect</span>
        <span class="token attr-name">v-model</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>form.person<span class="token punctuation">"</span></span>
        <span class="token attr-name">label</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>Person<span class="token punctuation">"</span></span>
        <span class="token attr-name">:options</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>[
            { id: 1, name: 'Danny MacAskill' },
            { id: 2, name: 'Stefano Meloni' },
            { id: 3, name: 'Toni Peperoni' },
        ]<span class="token punctuation">"</span></span>
    <span class="token punctuation">/></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>template</span><span class="token punctuation">></span></span>
</code></pre></div>
<p>In our <code>CustomListbox</code> component, on the top level <code>Listbox</code> component, we store the current option in a <code>selectedOption</code> ref, using <code>v-model</code>. As the current option is an object (e.g. <code>{id: 1, name: 'Danny MacAskill'}</code>), we need to extract the id from it and only pass this <code>Number</code> to the parent component.</p>
<p>So, in below example, we need to store the currently selected option anyway in a new ref <code>selectedOption</code>. Like this, we can make use of <code>v-model</code> on <code>Listbox</code> component and then watch that ref for changes. On every change event, we emit the <code>update:modelValue</code> event and only extract the <code>id</code> from the selected option. Here's our fully working (yet unstyled) <code>CustomListbox</code> component:</p>
<div class="nuxt-content-highlight"><pre class="language-html line-numbers"><code><span class="token comment">&#x3C;!-- CustomListbox.vue --></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>script</span> <span class="token attr-name">setup</span><span class="token punctuation">></span></span><span class="token script"><span class="token language-javascript">
<span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> ref<span class="token punctuation">,</span> watch <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'vue'</span>
<span class="token keyword module">import</span> <span class="token imports"><span class="token punctuation">{</span> <span class="token maybe-class-name">Listbox</span><span class="token punctuation">,</span> <span class="token maybe-class-name">ListboxLabel</span><span class="token punctuation">,</span> <span class="token maybe-class-name">ListboxButton</span><span class="token punctuation">,</span> <span class="token maybe-class-name">ListboxOptions</span><span class="token punctuation">,</span> <span class="token maybe-class-name">ListboxOption</span> <span class="token punctuation">}</span></span> <span class="token keyword module">from</span> <span class="token string">'@headlessui/vue'</span>

<span class="token keyword">const</span> props <span class="token operator">=</span> <span class="token function">defineProps</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
    <span class="token literal-property property">modelValue</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token known-class-name class-name">Number</span><span class="token punctuation">,</span>
        <span class="token keyword module">default</span><span class="token operator">:</span> <span class="token keyword null nil">null</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token literal-property property">label</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token known-class-name class-name">String</span><span class="token punctuation">,</span>
        <span class="token keyword module">default</span><span class="token operator">:</span> <span class="token string">''</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token literal-property property">options</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token literal-property property">type</span><span class="token operator">:</span> <span class="token known-class-name class-name">Array</span><span class="token punctuation">,</span>
        <span class="token literal-property property">required</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span><span class="token punctuation">)</span>

<span class="token keyword">const</span> emit <span class="token operator">=</span> <span class="token function">defineEmits</span><span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string">'update:modelValue'</span><span class="token punctuation">]</span><span class="token punctuation">)</span>

<span class="token keyword">const</span> selectedOption <span class="token operator">=</span> <span class="token function">ref</span><span class="token punctuation">(</span>props<span class="token punctuation">.</span><span class="token property-access">options</span><span class="token punctuation">[</span><span class="token number">0</span><span class="token punctuation">]</span> <span class="token operator">??</span> <span class="token keyword null nil">null</span><span class="token punctuation">)</span>

<span class="token function">watch</span><span class="token punctuation">(</span>selectedOption<span class="token punctuation">,</span> <span class="token punctuation">(</span><span class="token parameter">option</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token function">emit</span><span class="token punctuation">(</span><span class="token string">'update:modelValue'</span><span class="token punctuation">,</span> option<span class="token punctuation">.</span><span class="token property-access">id</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token punctuation">{</span> <span class="token literal-property property">immediate</span><span class="token operator">:</span> <span class="token boolean">true</span> <span class="token punctuation">}</span><span class="token punctuation">)</span>
</span></span><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>script</span><span class="token punctuation">></span></span>

<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>template</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Listbox</span> <span class="token attr-name">v-model</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>selectedOption<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>ListboxLabel</span><span class="token punctuation">></span></span>{{ label }}:<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>ListboxLabel</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>ListboxButton</span><span class="token punctuation">></span></span>{{ selectedOption.name }}<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>ListboxButton</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>ListboxOptions</span><span class="token punctuation">></span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>ListboxOption</span> <span class="token attr-name">v-for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>option in options<span class="token punctuation">"</span></span> <span class="token attr-name">:key</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>option<span class="token punctuation">"</span></span> <span class="token attr-name">:value</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>option<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
                {{ option.name }}
            <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>ListboxOption</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>ListboxOptions</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Listbox</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>template</span><span class="token punctuation">></span></span>
</code></pre></div>
<p>I hope these examples gave you a better feeling for <code>v-model</code>.</p>
<p>Now, enjoy the power of magic reactivity by two-way data binding. Happy coding!</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="frontend" term="frontend"/>
        <category label="vue.js" term="vue.js"/>
        <category label="js" term="js"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Configure ESLint and Prettier for Vue.js 3 in VS Code]]></title>
        <id>https://pipo.blog/articles/20220103-eslint-prettier-vue3</id>
        <link href="https://pipo.blog/articles/20220103-eslint-prettier-vue3"/>
        <updated>2022-01-03T17:30:00.000Z</updated>
        <summary type="html"><![CDATA[Quick and dirty VS Code setup of ESLint and Prettier for your next Vue 3 project.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/prettier.jpg"></p>
                      <p>I'd like to share my recommended <a href="https://eslint.org/" rel="nofollow noopener noreferrer" target="_blank">ESLint</a> and <a href="https://prettier.io/" rel="nofollow noopener noreferrer" target="_blank">Prettier</a> configuration for a <a href="https://v3.vuejs.org/" rel="nofollow noopener noreferrer" target="_blank">Vue.js 3</a> project. The two are not mutually exclusive – ESLint is for linting and Prettier handles code formatting. Believe me, you're going to use both! As preferred IDE, I am using <a href="https://code.visualstudio.com/" rel="nofollow noopener noreferrer" target="_blank">VS Code</a>. This article is just considered as simple dev notes without digging any deeper, and it's based on my current "best practices".</p>
<p>We're going to install Prettier first and ensure it's running fine. After that, we're going to install ESLint which is a bit trickier and needs some fine-tuning so that it won't conflict with any rules that Prettier already handles.</p>
<!--more-->
<blockquote>
<p>This is an update of my previous article <a href="/articles/20210805-eslint-prettier-for-vscode">Configure ESLint and Prettier for Vue/Nuxt.js</a>, adapted to a plain Vue 3 project with recommended setup for a fresh 2022 start. It is mainly based on the following in-depth tutorial: <a href="https://vueschool.io/articles/vuejs-tutorials/eslint-and-prettier-with-vite-and-vue-js-3/" rel="nofollow noopener noreferrer" target="_blank">ESLint and Prettier with Vite and Vue.js 3</a></p>
</blockquote>
<h2 id="prettier"><a href="#prettier" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Prettier</h2>
<h3 id="configure-prettier"><a href="#configure-prettier" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Configure Prettier</h3>
<p>Install <a href="https://prettier.io/" rel="nofollow noopener noreferrer" target="_blank">Prettier</a> into <code>devDependencies</code> using <code>npm</code> (or use <code>yarn add --dev</code> instead):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">npm</span> <span class="token function">install</span> --save-dev prettier
</code></pre></div>
<p>If you also want Prettier to handle PHP files (this is a frontend dev article only, but since <a href="https://inertiajs.com/" rel="nofollow noopener noreferrer" target="_blank">Inertia.js</a> is such a great project and motivates us to build single-page apps in the same repo, you might also have such a mix of Vue/PHP files...):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">npm</span> <span class="token function">install</span> --save-dev @prettier/plugin-php
</code></pre></div>
<p>Configure Prettier in <code>.prettierrc</code> (again in your project root):</p>
<div class="nuxt-content-highlight"><span class="filename">.prettierrc</span><pre class="line-numbers language-json"><code><span class="token punctuation">{</span>
    <span class="token property">"semi"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
    <span class="token property">"singleQuote"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
    <span class="token property">"tabWidth"</span><span class="token operator">:</span> <span class="token number">4</span><span class="token punctuation">,</span>
    <span class="token property">"printWidth"</span><span class="token operator">:</span> <span class="token number">120</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Also create <code>.prettierignore</code> (optional):</p>
<div class="nuxt-content-highlight"><span class="filename">.prettierignore</span><pre class="line-numbers language-ini"><code><span class="token comment"># Ignore artifacts:</span>
build
coverage
</code></pre></div>
<h3 id="vs-code-formatonsave"><a href="#vs-code-formatonsave" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>VS Code <code>formatOnSave</code></h3>
<p>Configure <a href="https://code.visualstudio.com/" rel="nofollow noopener noreferrer" target="_blank">VS Code</a> <strong>Editor: Format on Save</strong> to apply auto formatting upon saving, which you can also restrict to specific languages. I recommend only changing your workspace settings for the current project, which are stored in <code>.vscode/settings.json</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">.vscode/settings.json</span><pre class="line-numbers language-json"><code><span class="token punctuation">{</span>
    <span class="token property">"vetur.validation.template"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
    <span class="token property">"vetur.format.defaultFormatter.html"</span><span class="token operator">:</span> <span class="token string">"none"</span><span class="token punctuation">,</span>
    <span class="token comment">// Set the default</span>
    <span class="token property">"editor.formatOnSave"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
    <span class="token comment">// Enable per-language</span>
    <span class="token property">"[javascript]"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token property">"editor.formatOnSave"</span><span class="token operator">:</span> <span class="token boolean">true</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token property">"[vue]"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token property">"editor.formatOnSave"</span><span class="token operator">:</span> <span class="token boolean">true</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token property">"[php]"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token property">"editor.formatOnSave"</span><span class="token operator">:</span> <span class="token boolean">true</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Now, open any <code>.vue</code> or <code>.js</code> (or <code>.php</code>) file, remove some spacing to make it look ugly, fire <code>Cmd-S</code> to save the file, and see the magic. If it won't yet work for you, consider restarting VSCode.</p>
<p>Don't worry, a file only gets formatted if you explicitely hit <code>Cmd-S</code>. You can still use <code>autoSave</code> for regular saving, which would not invoke auto-formatting of the whole file. That's the relevant global VS Code user settings (can be found in <code>$HOME/Library/Application Support/Code/User/settings.json</code> on macOS):</p>
<div class="nuxt-content-highlight"><span class="filename">settings.json</span><pre class="line-numbers language-json"><code><span class="token punctuation">{</span>
    <span class="token property">"files.autoSave"</span><span class="token operator">:</span> <span class="token string">"afterDelay"</span><span class="token punctuation">,</span>
    <span class="token property">"prettier.printWidth"</span><span class="token operator">:</span> <span class="token number">81</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div>
<h2 id="configure-eslint"><a href="#configure-eslint" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Configure ESLint</h2>
<p>Install <a href="https://eslint.org/" rel="nofollow noopener noreferrer" target="_blank">ESLint</a> with all recommended plugins into <code>devDependencies</code> using <code>npm</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">npm</span> <span class="token function">install</span> --save-dev eslint eslint-plugin-vue eslint-config-prettier
$ <span class="token function">npm</span> <span class="token function">install</span> --save-dev eslint-plugin-prettier
</code></pre></div>
<p>Next, configure ESLint by creating an <code>.eslintrc.js</code> file with the following configuration so that it's configured to use  the ESLint recommend rules as well as the recommended rules for Vue 3. Minimal setup:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-js"><code>module<span class="token punctuation">.</span><span class="token property-access">exports</span> <span class="token operator">=</span> <span class="token punctuation">{</span>
  <span class="token literal-property property">env</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token literal-property property">node</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
  <span class="token punctuation">}</span><span class="token punctuation">,</span>
  <span class="token keyword">extends</span><span class="token operator">:</span> <span class="token punctuation">[</span>
    <span class="token string">'eslint:recommended'</span><span class="token punctuation">,</span>
    <span class="token string">'plugin:vue/vue3-recommended'</span><span class="token punctuation">,</span>
    <span class="token string">'prettier'</span><span class="token punctuation">,</span>
  <span class="token punctuation">]</span><span class="token punctuation">,</span>
  <span class="token literal-property property">rules</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token comment">// override/add rules settings here, such as:</span>
    <span class="token comment">// 'vue/no-unused-vars': 'error'</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div>
<blockquote>
<p>NOTE: The really important part here is, that you don't forget to register <a href="https://www.npmjs.com/package/eslint-config-prettier" rel="nofollow noopener noreferrer" target="_blank">eslint-config-prettier</a> which turns off all rules that are unnecessary or might conflict with Prettier. Make sure it's the last config defined in the <code>extends</code> array as the order of the configs determine duplicate rules (later rules override previous ones and eslint-config-prettier <em>only</em> turns rules <em>off</em>)!</p>
</blockquote>
<p>Visit the <a href="https://eslint.vuejs.org/user-guide/#usage" rel="nofollow noopener noreferrer" target="_blank">eslint-plugin-vue docs</a> to see the other configurations available for use with Vue 3 for less  strict options if desired. The different configurations correspond to  the 3 different priorities in the <a href="https://v3.vuejs.org/style-guide/" rel="nofollow noopener noreferrer" target="_blank">Vue.js 3 style guide</a>.</p>
<p>This is my <strong>recommended setup</strong> which makes use of <a href="https://www.npmjs.com/package/eslint-plugin-prettier" rel="nofollow noopener noreferrer" target="_blank">eslint-plugin-prettier</a> and defines some extra rules:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-js"><code>module<span class="token punctuation">.</span><span class="token property-access">exports</span> <span class="token operator">=</span> <span class="token punctuation">{</span>
    <span class="token literal-property property">root</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
    <span class="token literal-property property">env</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token literal-property property">browser</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token literal-property property">node</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token keyword">extends</span><span class="token operator">:</span> <span class="token punctuation">[</span>
        <span class="token string">'eslint:recommended'</span><span class="token punctuation">,</span>
        <span class="token string">'plugin:vue/vue3-recommended'</span><span class="token punctuation">,</span>
        <span class="token string">'prettier'</span>
    <span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token literal-property property">plugins</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'prettier'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token literal-property property">rules</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token string-property property">'prettier/prettier'</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'error'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        <span class="token string-property property">'vue/require-default-prop'</span><span class="token operator">:</span> <span class="token string">'off'</span><span class="token punctuation">,</span>
        <span class="token string-property property">'vue/html-indent'</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'error'</span><span class="token punctuation">,</span> <span class="token number">4</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        <span class="token string-property property">'vue/singleline-html-element-content-newline'</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span>
        <span class="token string-property property">'vue/component-name-in-template-casing'</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'error'</span><span class="token punctuation">,</span> <span class="token string">'PascalCase'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token literal-property property">globals</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token literal-property property">_</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Feel free to disable some of my rules, e.g. not everybody likes to have component names inside <code>&#x3C;template></code> tag in PascalCase.</p>
<p>Happy clean coding!</p>
<h2 id="review"><a href="#review" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Review</h2>
<p>Above configuration is based on the following versions, added lines to <code>package.json</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-json"><code><span class="token punctuation">{</span>
    <span class="token property">"devDependencies"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token property">"@prettier/plugin-php"</span><span class="token operator">:</span> <span class="token string">"^0.17.6"</span><span class="token punctuation">,</span>
        <span class="token property">"eslint"</span><span class="token operator">:</span> <span class="token string">"^8.6.0"</span><span class="token punctuation">,</span>
        <span class="token property">"eslint-config-prettier"</span><span class="token operator">:</span> <span class="token string">"^8.3.0"</span><span class="token punctuation">,</span>
        <span class="token property">"eslint-plugin-prettier"</span><span class="token operator">:</span> <span class="token string">"^4.0.0"</span><span class="token punctuation">,</span>
        <span class="token property">"eslint-plugin-vue"</span><span class="token operator">:</span> <span class="token string">"^8.2.0"</span><span class="token punctuation">,</span>
        <span class="token property">"prettier"</span><span class="token operator">:</span> <span class="token string">"^2.5.1"</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Be ready for your configuration to break on the next major release of ESLint! It can be a pain, sometimes.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="frontend" term="frontend"/>
        <category label="vue.js" term="vue.js"/>
        <category label="js" term="js"/>
        <category label="vscode" term="vscode"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Log emails as *.eml in Laravel Mailer]]></title>
        <id>https://pipo.blog/articles/20211203-laravel-mailer-log-eml</id>
        <link href="https://pipo.blog/articles/20211203-laravel-mailer-log-eml"/>
        <updated>2021-12-03T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Log complete email messages that are sent out by Laravel's Mailer.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/laravel-mailer.png"></p>
                      <p>How to log full email messages into <code>*.eml</code> files on filesystem in your Laravel project? We would like to log full message bodies for all messages, no matter if they are sent out directly using Laravel's <a href="https://laravel.com/docs/8.x/mail" rel="nofollow noopener noreferrer" target="_blank"><code>Mail</code> facade</a> or as <a href="https://laravel.com/docs/8.x/notifications#mail-notifications" rel="nofollow noopener noreferrer" target="_blank">Mail Notifications</a>. That can actually be implemented in a super easy way, but it was so hard to find any good documentation or tutorial about this.</p>
<!--more-->
<blockquote>
<p>NOTE: <a href="https://swiftmailer.symfony.com" rel="nofollow noopener noreferrer" target="_blank">Swift Mailer</a> is no longer maintained since Nov 2021. In Laravel 9 LTS, which is going to be released on Jan 25th 2022, Swift Mailer is going to be swapped out with <a href="https://symfony.com/blog/the-end-of-swiftmailer" rel="nofollow noopener noreferrer" target="_blank">Symfony Mailer</a>, see <a href="https://github.com/laravel/framework/pull/38481" rel="nofollow noopener noreferrer" target="_blank">PR #38481</a>. Below presented solution seems to be compatible with the new Mailer, though. I'll keep this post updated, if any changes are needed for Laravel 9.</p>
</blockquote>
<h2 id="simple-log-channel-implementation-dont"><a href="#simple-log-channel-implementation-dont" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Simple log channel implementation (DON'T)</h2>
<p>In this <a href="https://stackoverflow.com/a/64463470/5982842" rel="nofollow noopener noreferrer" target="_blank">Stackoverflow post</a>, I have presented a simple way of logging email messages.</p>
<p>Create logdir <code>logs/emails</code></p>
<p>Add an extra channel configuration to <code>config/logging.php</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-php"><code>    <span class="token string single-quoted-string">'channels'</span> <span class="token operator">=></span> <span class="token punctuation">[</span>
        <span class="token comment">// ...</span>
        <span class="token string single-quoted-string">'emails'</span> <span class="token operator">=></span> <span class="token punctuation">[</span>
            <span class="token string single-quoted-string">'driver'</span> <span class="token operator">=></span> <span class="token string single-quoted-string">'single'</span><span class="token punctuation">,</span>
            <span class="token string single-quoted-string">'path'</span> <span class="token operator">=></span> <span class="token function">storage_path</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'logs/emails/'</span> <span class="token operator">.</span> <span class="token class-name class-name-fully-qualified static-context"><span class="token punctuation">\</span>Illuminate<span class="token punctuation">\</span>Support<span class="token punctuation">\</span>Str</span><span class="token operator">::</span><span class="token function">uuid</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token operator">.</span> <span class="token string single-quoted-string">'.eml'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
            <span class="token string single-quoted-string">'level'</span> <span class="token operator">=></span> <span class="token string single-quoted-string">'debug'</span><span class="token punctuation">,</span>
        <span class="token punctuation">]</span><span class="token punctuation">,</span>
</code></pre></div>
<p>And then simply use the following in your <code>.env</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token assign-left variable">MAIL_MAILER</span><span class="token operator">=</span>log
<span class="token assign-left variable">MAIL_LOG_CHANNEL</span><span class="token operator">=</span>emails
</code></pre></div>
<p>That way, you get wonderful <code>logs/emails/&#x3C;UUID>.eml</code> files. But this solution has several drawbacks:</p>
<ul>
<li>There is no easy way of sending out emails both through <code>MAIL_MAILER=log</code> and some other mailer, e.g. the standard <code>MAIL_MAILER=smtp</code></li>
<li>Writing more than one message to the <code>email</code> log channel during the same run would not trigger an updated UUID, so the second message would get appended to the same logfile. This happens e.g. if you trigger a notification in Tinker.</li>
<li>The first line (before <code>Message-ID: ...</code>) of the email header is prefixed with log formatting prefix <code>[_TIMESTAMP_] local.DEBUG: </code>. As a non-standard email header is ignored by all/most email clients, this should be no issue in development. But definitely not cool!</li>
<li>The <code>*.eml</code> message body is scrambled, somehow. The MIME multipart content type boundary gets lost, so the email client has no idea where the HTML part of the message body starts and which part to treat as plaintext.</li>
</ul>
<p>So, this solution is rejected! We need some slightly more sophisticated logging!</p>
<h2 id="logsentmessage-event-listener"><a href="#logsentmessage-event-listener" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a><code>LogSentMessage</code> event listener</h2>
<p>Let's forget about the <code>emails</code> log channel from above solution and create the following disk configuration instead, <code>config/filesystems.php</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-php"><code><span class="token keyword">return</span> <span class="token punctuation">[</span>
    <span class="token comment">// ...</span>
    <span class="token string single-quoted-string">'disks'</span> <span class="token operator">=></span> <span class="token punctuation">[</span>
        <span class="token comment">// ...</span>
        <span class="token string single-quoted-string">'emails'</span> <span class="token operator">=></span> <span class="token punctuation">[</span>
            <span class="token string single-quoted-string">'driver'</span>     <span class="token operator">=></span> <span class="token string single-quoted-string">'local'</span><span class="token punctuation">,</span>
            <span class="token string single-quoted-string">'root'</span>       <span class="token operator">=></span> <span class="token function">storage_path</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'logs/emails'</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
        <span class="token punctuation">]</span><span class="token punctuation">,</span>
</code></pre></div>
<p>Instead of changing the Mailer in <code>.env</code> (keep whatever <code>MAIL_MAILER</code> you are used to!), we build our <code>LogSentMessage</code> event listener and use Laravel's <a href="https://laravel.com/docs/8.x/mail#events" rel="nofollow noopener noreferrer" target="_blank"><code>MessageSent</code> event</a>.</p>
<p>Create <code>app/Listeners/LogSentMessage.php</code>, which is going to write the full message:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-php"><code><span class="token php language-php"><span class="token delimiter important">&#x3C;?php</span>

<span class="token keyword">namespace</span> <span class="token package">App<span class="token punctuation">\</span>Listeners</span><span class="token punctuation">;</span>

<span class="token keyword">use</span> <span class="token package">Illuminate<span class="token punctuation">\</span>Mail<span class="token punctuation">\</span>Events<span class="token punctuation">\</span>MessageSent</span><span class="token punctuation">;</span>
<span class="token keyword">use</span> <span class="token package">Illuminate<span class="token punctuation">\</span>Support<span class="token punctuation">\</span>Facades<span class="token punctuation">\</span>Storage</span><span class="token punctuation">;</span>
<span class="token keyword">use</span> <span class="token package">Illuminate<span class="token punctuation">\</span>Support<span class="token punctuation">\</span>Str</span><span class="token punctuation">;</span>

<span class="token keyword">class</span> <span class="token class-name-definition class-name">LogSentMessage</span>
<span class="token punctuation">{</span>
    <span class="token keyword">public</span> <span class="token keyword">function</span> <span class="token function-definition function">handle</span><span class="token punctuation">(</span><span class="token class-name type-declaration">MessageSent</span> <span class="token variable">$event</span><span class="token punctuation">)</span>
    <span class="token punctuation">{</span>
        <span class="token variable">$messageId</span> <span class="token operator">=</span> <span class="token variable">$event</span><span class="token operator">-></span><span class="token property">data</span><span class="token punctuation">[</span><span class="token string single-quoted-string">'__laravel_notification_id'</span><span class="token punctuation">]</span> <span class="token operator">??</span> <span class="token scope">Str<span class="token punctuation">::</span></span><span class="token function">uuid</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
        <span class="token scope">Storage<span class="token punctuation">::</span></span><span class="token function">disk</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'emails'</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">put</span><span class="token punctuation">(</span>
            <span class="token function">sprintf</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'%s_%s.eml'</span><span class="token punctuation">,</span> <span class="token function">now</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-></span><span class="token function">format</span><span class="token punctuation">(</span><span class="token string single-quoted-string">'YmdHis'</span><span class="token punctuation">)</span><span class="token punctuation">,</span> <span class="token variable">$messageId</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
            <span class="token variable">$event</span><span class="token operator">-></span><span class="token property">message</span><span class="token operator">-></span><span class="token function">toString</span><span class="token punctuation">(</span><span class="token punctuation">)</span>
        <span class="token punctuation">)</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</span></code></pre></div>
<p>The <code>LogSentMessage</code> event listener can now be hooked into  <code>app/Providers/EventServiceProvider.php</code></p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-php"><code><span class="token php language-php"><span class="token delimiter important">&#x3C;?php</span>

<span class="token keyword">namespace</span> <span class="token package">App<span class="token punctuation">\</span>Providers</span><span class="token punctuation">;</span>

<span class="token keyword">use</span> <span class="token package">App<span class="token punctuation">\</span>Listeners<span class="token punctuation">\</span>LogSentMessage</span><span class="token punctuation">;</span>
<span class="token keyword">use</span> <span class="token package">Illuminate<span class="token punctuation">\</span>Foundation<span class="token punctuation">\</span>Support<span class="token punctuation">\</span>Providers<span class="token punctuation">\</span>EventServiceProvider</span> <span class="token keyword">as</span> ServiceProvider<span class="token punctuation">;</span>
<span class="token keyword">use</span> <span class="token package">Illuminate<span class="token punctuation">\</span>Mail<span class="token punctuation">\</span>Events<span class="token punctuation">\</span>MessageSent</span><span class="token punctuation">;</span>

<span class="token keyword">class</span> <span class="token class-name-definition class-name">EventServiceProvider</span> <span class="token keyword">extends</span> <span class="token class-name">ServiceProvider</span>
<span class="token punctuation">{</span>
    <span class="token doc-comment comment">/**
     * The event listener mappings for the application.
     *
     * <span class="token keyword">@var</span> <span class="token class-name"><span class="token keyword">array</span></span>
     */</span>
    <span class="token keyword">protected</span> <span class="token variable">$listen</span> <span class="token operator">=</span> <span class="token punctuation">[</span>
        <span class="token scope">MessageSent<span class="token punctuation">::</span></span><span class="token keyword">class</span> <span class="token operator">=></span> <span class="token punctuation">[</span>
            <span class="token scope">LogSentMessage<span class="token punctuation">::</span></span><span class="token keyword">class</span><span class="token punctuation">,</span>
        <span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

</span></code></pre></div>
<blockquote>
<p>To generate the listener class, you could simply add these lines to <code>EventServiceProvider.php</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-diff"><code><span class="token unchanged"><span class="token prefix unchanged"> </span>   protected $listen = [
</span><span class="token inserted-sign inserted"><span class="token prefix inserted">+</span>        MessageSent::class => [
<span class="token prefix inserted">+</span>            'App\Listeners\LogSentMessage',
<span class="token prefix inserted">+</span>        ],
</span><span class="token unchanged"><span class="token prefix unchanged"> </span>   ];
</span></code></pre></div>
<p>... and then fire:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ php artisan event:generate
</code></pre></div>
</blockquote>
<p>This works great! Email notifications/messages are now getting sent out normally and at the same time a copy of the full message is stored here:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>storage/logs/emails
├── 20211203131816_385ce6e8-437c-427e-828c-7464a4a48ca2.eml
└── 20211203132656_757c62b6-b6fa-45d0-99e3-6caf5be6a9f3.eml
</code></pre></div>
<p>You can open or even import them with any mail client and they are just 100% valid emails, including all headers (also Bcc). Just make sure this storage path is never publicly exposed.</p>
<p>Thanks <a href="https://stackoverflow.com/a/60147190/5982842" rel="nofollow noopener noreferrer" target="_blank">@davewood</a> for this simple trick.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="laravel" term="laravel"/>
        <category label="php" term="php"/>
        <category label="backend" term="backend"/>
        <category label="email" term="email"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Fail2ban jail to mitigate DoS attacks against Apache]]></title>
        <id>https://pipo.blog/articles/20210915-fail2ban-apache-dos</id>
        <link href="https://pipo.blog/articles/20210915-fail2ban-apache-dos"/>
        <updated>2021-09-15T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Using a Fail2ban jail to mitigate simple DoS attacks against Apache.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/fail2ban-apache-dos.png"></p>
                      <p>Recently, one of our shared hosting webservers at <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime GmbH</a> got hit by a DoS attack. The attacker started a larger vulnerability scan against common Wordpress security issues. We already had common brute-force attack patterns on Wordpress covered by a custom <a href="https://www.fail2ban.org" rel="nofollow noopener noreferrer" target="_blank">Fail2Ban</a> jail, which mainly trapped <code>POST</code> requests to <code>xmlrpc.php</code> or <code>wp-login.php</code> (the usual dumb WP brute-force attacks...). But this DoS attack had hundreds of customer sites as target and did not get trapped by our existing rules.</p>
<p>After having blocked the attacker's IP (glad this was no large-scale DDoS!), I wrote an extra Fail2Ban jail which traps such simple DoS attacks. It's a very basic Fail2Ban jail that should cover common attacks and should not cause any false positives as it is only getting triggered by a large amount of failed <code>GET</code> requests.</p>
<!--more-->
<p>There are other good articles about setting up such Fail2Ban jails to block simple DoS, but they didn't quite fit our needs:</p>
<ul>
<li><a href="https://samnicholls.net/2016/06/06/fail2ban-apache-dos/" rel="nofollow noopener noreferrer" target="_blank">Using fail2ban to mitigate simple DOS attacks against apache (or why I am a terrible sysop)</a></li>
<li><a href="https://www.garron.me/en/go2linux/fail2ban-protect-web-server-http-dos-attack.html" rel="nofollow noopener noreferrer" target="_blank">Install fail2ban to protect your site from DOS attacks</a></li>
</ul>
<h2 id="requirements"><a href="#requirements" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Requirements</h2>
<p>What we would like to accomplish:</p>
<ul>
<li>Scanning of all Apache access logs</li>
<li>Ban the attackers IP if there are more than 300 <code>GET</code> requests during a time span of 5 mins resulting in HTTP non-200 (OK) status codes: 401, 403, 404, 503</li>
<li>Ban the attacker for 1 hour</li>
</ul>
<p>We're going to scan all customer Apache <code>access.log</code> logfiles which are in a slightly tuned (non-standard) <code>combined</code> log format:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-apacheconf"><code><span class="token comment">#LogFormat "%h %l %u %t \"%r\" %>s %O \"%{Referer}i\" \"%{User-Agent}i\"" combined</span>
<span class="token directive-inline property">LogFormat</span> <span class="token string">"<span class="token variable">%h</span> <span class="token variable">%l</span> <span class="token variable">%u</span> <span class="token variable">%t</span> <span class="token variable">%I</span> <span class="token variable">%O</span> \"<span class="token variable">%r</span>\" %>s <span class="token variable">%b</span> \"<span class="token variable">%{Referer}</span>i\" \"<span class="token variable">%{User-Agent}</span>i\" <span class="token variable">%V</span> <span class="token variable">%p</span>"</span> combined
</code></pre></div>
<p>Below <code>failregex</code> also works for default <code>extended/combined</code> or <code>common</code> Apache <a href="https://httpd.apache.org/docs/2.4/mod/mod_log_config.html#logformat" rel="nofollow noopener noreferrer" target="_blank"><code>LogFormat</code></a>.</p>
<h2 id="setup-fail2ban-jail"><a href="#setup-fail2ban-jail" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Setup Fail2Ban jail</h2>
<p>Let's call the new Fail2Ban jail <code>apache-get-dos</code> and create the following two files:</p>
<div class="nuxt-content-highlight"><span class="filename">jail.d/apache-get-dos.conf</span><pre class="line-numbers language-ini"><code><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">apache-get-dos</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">enabled</span>  <span class="token punctuation">=</span> <span class="token value attr-value">true</span>
<span class="token key attr-name">port</span>     <span class="token punctuation">=</span> <span class="token value attr-value">http,https</span>
<span class="token key attr-name">filter</span>   <span class="token punctuation">=</span> <span class="token value attr-value">apache-get-dos</span>
<span class="token key attr-name">logpath</span>  <span class="token punctuation">=</span> <span class="token value attr-value">/var/www/*/logs/access.log</span>
<span class="token key attr-name">datepattern</span> <span class="token punctuation">=</span> <span class="token value attr-value">%%d/%%b/%%Y:%%H:%%M:%%S %%z</span>
<span class="token key attr-name">maxretry</span> <span class="token punctuation">=</span> <span class="token value attr-value">300</span>
<span class="token key attr-name">findtime</span> <span class="token punctuation">=</span> <span class="token value attr-value">5m</span>
<span class="token key attr-name">bantime</span>  <span class="token punctuation">=</span> <span class="token value attr-value">1h</span>
</code></pre></div>
<blockquote>
<p>In above jail, we didn't specify a <code>banaction</code>, as we're using the global default. You might want to put the following in <code>jail.d/custom.conf</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">jail.d/custom.conf</span><pre class="line-numbers language-ini"><code><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">DEFAULT</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">bantime</span>  <span class="token punctuation">=</span> <span class="token value attr-value">300</span>
<span class="token key attr-name">findtime</span> <span class="token punctuation">=</span> <span class="token value attr-value">300</span>
<span class="token key attr-name">banaction</span> <span class="token punctuation">=</span> <span class="token value attr-value">iptables-allports</span>
</code></pre></div>
</blockquote>
<div class="nuxt-content-highlight"><span class="filename">filter.d/apache-get-dos.conf</span><pre class="line-numbers language-ini"><code><span class="token comment"># Fail2Ban filter to scan Apache access.log for DoS attacks</span>

<span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">INCLUDES</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">before</span> <span class="token punctuation">=</span> <span class="token value attr-value">common.conf</span>

<span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Definition</span><span class="token punctuation">]</span></span>
<span class="token comment"># Option:  failregex</span>
<span class="token comment"># Notes.:  regex to match GET requests in the logfile resulting in one of the</span>
<span class="token comment">#          following status codes: 401, 403, 404, 503.</span>
<span class="token comment">#          The host must be matched by a group named "host". The tag "&#x3C;HOST>" </span>
<span class="token comment">#          can be used for standard IP/hostname matching and is only an alias for</span>
<span class="token comment">#          (?:::f{4,6}:)?(?P&#x3C;host>[\w\-.^_]+)</span>
<span class="token comment"># Values:  TEXT</span>
<span class="token key attr-name">failregex</span> <span class="token punctuation">=</span> <span class="token value attr-value">^&#x3C;HOST> .*"GET (?!\/robots\.txt).*" (401|403|404|503)\s</span>

<span class="token comment"># Option:  ignoreregex</span>
<span class="token comment"># Notes.:  regex to ignore. If this regex matches, the line is ignored.</span>
<span class="token comment"># Values:  TEXT</span>
<span class="token comment">#</span>
<span class="token key attr-name">ignoreregex</span> <span class="token punctuation">=</span>
</code></pre></div>
<p>You might want to tune the <code>failregex</code> to your needs. For our use case, I have created a <a href="https://regex101.com/r/F1Z9VO/1" rel="nofollow noopener noreferrer" target="_blank">regex101</a> with some sample log lines which you can play around with. It will ignore any failing <code>GET /robots.txt</code> requests (as most bots request this) and it will not care about "sane" HTTP status codes like 200 (OK), 301 (Moved Permanently), 302 (Found). So, please be aware, that this jail only protects you from some kind of DoS attack that targets a large amount of non-existing URLs/pathes, which is mostly the case for malicious vulnerability scans.</p>
<h2 id="monitoring-jail"><a href="#monitoring-jail" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Monitoring jail</h2>
<p>After having restarted Fail2Ban, you might want to monitor the new <code>apache-get-dos</code> jail:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token comment"># list status of apache-get-dos jail with monitored Apache logs</span>
$ fail2ban-client status apache-get-dos
Status <span class="token keyword">for</span> the jail: apache-get-dos
<span class="token operator">|</span>- Filter
<span class="token operator">|</span>  <span class="token operator">|</span>- Currently failed: <span class="token number">80</span>
<span class="token operator">|</span>  <span class="token operator">|</span>- Total failed: <span class="token number">290517</span>
<span class="token operator">|</span>  <span class="token variable"><span class="token variable">`</span>- File list:    /var/www/<span class="token operator">&#x3C;</span>user<span class="token operator">></span>/logs/access.log <span class="token punctuation">..</span>.
<span class="token variable">`</span></span>- Actions
   <span class="token operator">|</span>- Currently banned: <span class="token number">0</span>
   <span class="token operator">|</span>- Total banned: <span class="token number">5</span>
   `- Banned IP list:   

<span class="token comment"># review banning in fail2ban.log</span>
$ <span class="token function">grep</span> <span class="token parameter variable">-Pi</span> <span class="token string">'apache-get-dos\] (un)?ban'</span> /var/log/fail2ban.log
<span class="token number">2021</span>-09-14 <span class="token number">17</span>:45:47,285 fail2ban.actions        <span class="token punctuation">[</span><span class="token number">107006</span><span class="token punctuation">]</span>: NOTICE  <span class="token punctuation">[</span>apache-get-dos<span class="token punctuation">]</span> Ban <span class="token number">1.2</span>.3.4
<span class="token number">2021</span>-09-14 <span class="token number">18</span>:45:45,503 fail2ban.actions        <span class="token punctuation">[</span><span class="token number">107006</span><span class="token punctuation">]</span>: NOTICE  <span class="token punctuation">[</span>apache-get-dos<span class="token punctuation">]</span> Unban <span class="token number">1.2</span>.3.4
</code></pre></div>
<p>In case you have designed your <code>failregex</code> too aggressively and have banned too many IPs, remember those commands:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token comment"># unban an IP from a specific jail</span>
$ fail2ban-client <span class="token builtin class-name">set</span> <span class="token operator">&#x3C;</span>JAIL<span class="token operator">></span> unbanip <span class="token operator">&#x3C;</span>IP<span class="token operator">></span>

<span class="token comment"># unban an IP (or several) from all jails</span>
$ fail2ban-client unban <span class="token operator">&#x3C;</span>IP<span class="token operator">></span> <span class="token punctuation">..</span>. <span class="token operator">&#x3C;</span>IP<span class="token operator">></span>

<span class="token comment"># unbans all IP addresses (in all jails and database)</span>
$ fail2ban-client unban <span class="token parameter variable">--all</span>
</code></pre></div>
<h2 id="bonus-commands"><a href="#bonus-commands" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Bonus Commands</h2>
<p>I'll provide you with some bonus commands to further investigate such DoS attacks:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token comment"># Top 20 list of IPs with a large amount of connections to the server</span>
$ <span class="token function">netstat</span> <span class="token parameter variable">-ntu</span> <span class="token operator">|</span> <span class="token function">awk</span> <span class="token string">'{print $5}'</span> <span class="token operator">|</span> <span class="token function">cut</span> -d: <span class="token parameter variable">-f1</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token operator">|</span> <span class="token function">uniq</span> <span class="token parameter variable">-c</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token parameter variable">-nr</span> <span class="token operator">|</span> <span class="token function">head</span> <span class="token parameter variable">-n20</span>
</code></pre></div>
<p>You may also want to scan all Apache access logs for the IPs with most requests:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token comment"># Top 10 IP list for ALL sites for previous hour</span>
$ <span class="token function">grep</span> <span class="token parameter variable">-h</span> <span class="token string">"\[<span class="token variable"><span class="token variable">$(</span><span class="token function">date</span> <span class="token parameter variable">-d</span> <span class="token parameter variable">-1hour</span> +<span class="token string">'%d/%b/%Y:%H:'</span><span class="token variable">)</span></span>"</span> /var/www/*/logs/access.log <span class="token operator">|</span> <span class="token function">cut</span> -d<span class="token string">' '</span> <span class="token parameter variable">-f1</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token operator">|</span> <span class="token function">uniq</span> <span class="token parameter variable">-c</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token parameter variable">-nr</span> <span class="token operator">|</span> <span class="token function">head</span> <span class="token parameter variable">-n10</span>

<span class="token comment"># Top 10 IP list for ALL sites for current hour</span>
$ <span class="token function">grep</span> <span class="token parameter variable">-h</span> <span class="token string">"\[<span class="token variable"><span class="token variable">$(</span><span class="token function">date</span> +<span class="token string">'%d/%b/%Y:%H:'</span><span class="token variable">)</span></span>"</span> /var/www/*/logs/access.log <span class="token operator">|</span> <span class="token function">cut</span> -d<span class="token string">' '</span> <span class="token parameter variable">-f1</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token operator">|</span> <span class="token function">uniq</span> <span class="token parameter variable">-c</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token parameter variable">-nr</span> <span class="token operator">|</span> <span class="token function">head</span> <span class="token parameter variable">-n10</span>
 
<span class="token comment"># Top 10 IP list for ALL sites with filename</span>
$ <span class="token function">grep</span> <span class="token string">"\[<span class="token variable"><span class="token variable">$(</span><span class="token function">date</span> +<span class="token string">'%d/%b/%Y:%H:'</span><span class="token variable">)</span></span>"</span> /var/www/*/logs/access.log <span class="token operator">|</span> <span class="token function">cut</span> -d<span class="token string">' '</span> <span class="token parameter variable">-f1</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token operator">|</span> <span class="token function">uniq</span> <span class="token parameter variable">-c</span> <span class="token operator">|</span> <span class="token function">sort</span> <span class="token parameter variable">-nr</span> <span class="token operator">|</span> <span class="token function">head</span> <span class="token parameter variable">-n10</span>
</code></pre></div>
<p>I recommend you block such IPs directly on your front-end firewall. But in case you can't, use <code>iptables</code> to quickly block a single IP:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token comment"># Block a single IP</span>
$ iptables <span class="token parameter variable">-A</span> INPUT <span class="token parameter variable">-s</span> <span class="token operator">&#x3C;</span>IP<span class="token operator">></span> <span class="token parameter variable">-j</span> DROP
<span class="token comment"># Unblock it</span>
$ iptables <span class="token parameter variable">-D</span> INPUT <span class="token parameter variable">-s</span> <span class="token operator">&#x3C;</span>IP<span class="token operator">></span> <span class="token parameter variable">-j</span> DROP
</code></pre></div>
<p>And now, cross fingers you won't get hit by a brutal DDoS. I plan to cover DDoS attack mitigation in a future article.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="security" term="security"/>
        <category label="fail2ban" term="fail2ban"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[GitLab PostgreSQL Data Recovery]]></title>
        <id>https://pipo.blog/articles/20210901-gitlab-postgresql-recovery</id>
        <link href="https://pipo.blog/articles/20210901-gitlab-postgresql-recovery"/>
        <updated>2021-09-01T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Recovering PostgreSQL data for GitLab by a full dump/restore.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/gitlab-meets-postgresql.png"></p>
                      <p>Today, shit happened on a larger on-premise <a href="https://about.gitlab.com/" rel="nofollow noopener noreferrer" target="_blank">GitLab EE</a> instance of one of our <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime GmbH</a> customers. GitLab's production.log started to fill up with <code>PG::Error (FATAL: the database system is in recovery mode)</code> errors which were somehow related to LFS operations. That definitely didn't sound cool and smelled like data corruption. The customer noticed it by failed CI jobs with 500 Internal Server Errors, and let me know immediately.</p>
<p>As we have that GitLab server running in a LXC container on a <a href="https://openzfs.org" rel="nofollow noopener noreferrer" target="_blank">ZFS</a> based system (<a href="https://proxmox.com/en/proxmox-ve" rel="nofollow noopener noreferrer" target="_blank">Proxmox VE</a>), it was easy to pull a clone of the full system and play around with PostgreSQL data recovery before working on live data. I decided to go for a full data restore by dumping and loading it from scratch in a freshly initialized PostgreSQL data dir.</p>
<!--more-->
<p>So these were the <code>FATAL</code> errors I found in <code>/var/log/gitlab/gitlab-rails/production.log</code> (this is a Omnibus install of GitLab EE):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>Processing by Repositories::LfsStorageController#download as HTML
  Parameters: {"repository_path"=>"group/project.git", "oid"=>"38387ae38..."}
PG::Error (FATAL:  the database system is in recovery mode
):

Started GET "/group/project.git/gitlab-lfs/objects/372ea928b2..." for 192.168.x.x at 2021-09-01 09:19:31 +0200
ActiveRecord::ConnectionNotEstablished (FATAL:  the database system is in recovery mode
):
</code></pre></div>
<p>The errors were always preceded by some LFS operation, so that looks correlated.</p>
<blockquote>
<p><strong>UPDATE 2021-09-01 23:50</strong> – In the meantime (after the full recovery below - Shame on me ignorant sysadmin!), I have figured out this was actually caused by an OOM issue of that LXC container, where random PostgreSQL processes were killed by the system's oom-killer...</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code># /var/log/gitlab/postgresql/current
2021-09-01_07:19:31.05398 LOG:  server process (PID 10068) was terminated by signal 9: Killed

# /var/log/syslog
Sep  1 09:19:31 git kernel: [5068062.072130]  oom_kill_process.cold.33+0xb/0x10
</code></pre></div>
<p>But I still keep this article to explain how to do a full PostgreSQL data recovery, even though we never had any data corruption. :)</p>
</blockquote>
<h2 id="check-postgresql-tools"><a href="#check-postgresql-tools" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Check PostgreSQL Tools</h2>
<p>First, check your PostgreSQL version and figure out where GitLab Omnibus install put all those commands:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ gitlab-psql <span class="token parameter variable">--version</span>
psql <span class="token punctuation">(</span>PostgreSQL<span class="token punctuation">)</span> <span class="token number">12.6</span>

$ gitlab-psql <span class="token parameter variable">-c</span> <span class="token string">'select version()'</span>
PostgreSQL <span class="token number">12.6</span> on x86_64-pc-linux-gnu, <span class="token punctuation">..</span>.
</code></pre></div>
<p>Check database sizes:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ gitlab-psql <span class="token parameter variable">-c</span> <span class="token string">'SELECT pg_database.datname as "dbname", pg_database_size(pg_database.datname)/1024/1024 AS size_in_mb FROM pg_database ORDER by size_in_mb DESC'</span>
       dbname        <span class="token operator">|</span> size_in_mb 
---------------------+------------
 gitlabhq_production <span class="token operator">|</span>      <span class="token number">29151</span>
 template0           <span class="token operator">|</span>          <span class="token number">7</span>
 template1           <span class="token operator">|</span>          <span class="token number">7</span>
 postgres            <span class="token operator">|</span>          <span class="token number">7</span>
</code></pre></div>
<p>You'll find the following symlink:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>/usr/bin/gitlab-psql -> /opt/gitlab/bin/gitlab-psql
</code></pre></div>
<p>The other commands are here:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>/opt/gitlab/embedded/bin/pg_dump
/opt/gitlab/embedded/bin/pg_dumpall
</code></pre></div>
<p>But DON'T try to do a dump as <code>root</code>, always do it under user <code>gitlab-psql</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">su</span> - gitlab-psql

<span class="token punctuation">(</span>gitlab-psql<span class="token punctuation">)</span>$ <span class="token builtin class-name">echo</span> <span class="token environment constant">$PWD</span>           
/var/opt/gitlab/postgresql
<span class="token punctuation">(</span>gitlab-psql<span class="token punctuation">)</span>$ <span class="token function">which</span> pg_dumpall
/opt/gitlab/embedded/bin/pg_dumpall

<span class="token comment"># connect over socket</span>
<span class="token punctuation">(</span>gitlab-psql<span class="token punctuation">)</span>$ pg_dumpall <span class="token parameter variable">-h</span> /var/opt/gitlab/postgresql
<span class="token comment"># or connect over port</span>
<span class="token punctuation">(</span>gitlab-psql<span class="token punctuation">)</span>$ pg_dumpall <span class="token parameter variable">-h</span> localhost <span class="token parameter variable">-p</span> <span class="token number">9187</span>
</code></pre></div>
<p>If you're unsure about the port your PostgreSQL is running, check <code>netstat -tulpen</code> (don't need to visit the Netherlands for that!) and grab it from there. If you don't find any running postgres process that binds to some port, try to connect to the socket directly with <code>-h /var/opt/gitlab/postgresql</code>.</p>
<p>For below recovery, consult the following PostgreSQL 12 documentation:</p>
<ul>
<li><a href="https://www.postgresql.org/docs/12/app-pg-dumpall.html" rel="nofollow noopener noreferrer" target="_blank"><code>pg_dumpall</code></a></li>
<li><a href="https://www.postgresql.org/docs/12/app-initdb.html" rel="nofollow noopener noreferrer" target="_blank"><code>initdb</code></a></li>
<li><a href="https://www.postgresql.org/docs/12/backup-dump.html" rel="nofollow noopener noreferrer" target="_blank">PostgreSQL Backup and Restore</a></li>
</ul>
<h2 id="data-recovery-migration"><a href="#data-recovery-migration" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Data Recovery Migration</h2>
<h3 id="the-idea"><a href="#the-idea" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>The Idea</h3>
<ol>
<li>Stop all GitLab services, only keep PostgreSQL running</li>
<li>Make a full data dump with <a href="https://www.postgresql.org/docs/12/app-pg-dumpall.html" rel="nofollow noopener noreferrer" target="_blank"><code>pg_dumpall</code></a></li>
<li>Stop PostgreSQL</li>
<li>Move current PostgreSQL data dir away, so we can start from scratch</li>
<li>Re-initialize PostgreSQL data dir with <a href="https://www.postgresql.org/docs/12/app-initdb.html" rel="nofollow noopener noreferrer" target="_blank"><code>initdb</code></a></li>
<li>Start PostgreSQL</li>
<li>Restore full data dump by feeding it to <code>gitlab-psql</code></li>
<li>Stop all GitLab services, reconfigure GitLab and restart all services</li>
</ol>
<h3 id="dump--restore"><a href="#dump--restore" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Dump / Restore</h3>
<p>Stop all GitLab services and only keep PostgreSQL running:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ gitlab-ctl stop
$ gitlab-ctl start postgresql
$ gitlab-ctl status

<span class="token comment"># You might also want to stop cron, so it won't interfere your migration</span>
$ systemctl stop <span class="token function">cron</span> <span class="token operator">&#x26;&#x26;</span> systemctl disable <span class="token function">cron</span>
<span class="token comment"># Don't forget to start and re-enable it again after migration!</span>
</code></pre></div>
<blockquote>
<p><strong>To repeat</strong> (I found this quite confusing as PostgreSQL does not always bind to a port):</p>
<p>After starting <code>postgresql</code>, always use <code>netstat -tulpen</code> to check on which port it is running. If it's not running on a port, connect to the socket with <code>-h /var/opt/gitlab/postgresql</code> instead of <code>-h localhost -p &#x3C;PORT></code>, see examples below.</p>
</blockquote>
<p>Now, make full gzipped dump with <a href="https://www.postgresql.org/docs/12/app-pg-dumpall.html" rel="nofollow noopener noreferrer" target="_blank"><code>pg_dumpall</code></a>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">su</span> - gitlab-psql
<span class="token punctuation">(</span>gitlab-psql<span class="token punctuation">)</span>$ pg_dumpall <span class="token parameter variable">-h</span> /var/opt/gitlab/postgresql <span class="token operator">|</span> <span class="token function">gzip</span> <span class="token operator">></span> pg_dumpall.sql.gz
</code></pre></div>
<p>Re-create PostgreSQL data dir <code>/var/opt/gitlab/postgresql/data</code> from scratch with <a href="https://www.postgresql.org/docs/12/app-initdb.html" rel="nofollow noopener noreferrer" target="_blank"><code>initdb</code></a>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ gitlab-ctl stop postgresql

$ <span class="token function">su</span> - gitlab-psql
<span class="token punctuation">(</span>gitlab-psql<span class="token punctuation">)</span>$ <span class="token function">mv</span> data data.12.6.BKUP
<span class="token punctuation">(</span>gitlab-psql<span class="token punctuation">)</span>$ initdb data

$ gitlab-ctl start postgresql
</code></pre></div>
<p>(<code>$PWD</code> of <code>gitlab-psql</code> user is already <code>/var/opt/gitlab/postgresql</code>, so you don't need to use full pathes in that context)</p>
<p>Restore dump:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">su</span> - gitlab-psql
<span class="token punctuation">(</span>gitlab-psql<span class="token punctuation">)</span>$ zcat pg_dumpall.sql.gz <span class="token operator">|</span> gitlab-psql <span class="token parameter variable">-h</span> localhost <span class="token parameter variable">-p</span> <span class="token number">5432</span> <span class="token parameter variable">-d</span> postgres
</code></pre></div>
<blockquote>
<p><strong>NOTE:</strong></p>
<p><a href="https://www.postgresql.org/docs/12/backup-dump.html#BACKUP-DUMP-ALL" rel="nofollow noopener noreferrer" target="_blank">SQL Dump: Using pg_dumpall</a>: Actually, you can specify any existing database name to start from, but if you are loading into an empty cluster then <code>postgres</code> should usually be used.</p>
</blockquote>
<p>Stop <code>postgresql</code> and reconfigure GitLab, then start all GitLab services:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ gitlab-ctl stop postgresql
$ gitlab-ctl reconfigure
$ gitlab-ctl start
</code></pre></div>
<p>Monitor logs:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">tail</span> <span class="token parameter variable">-f</span> /var/log/gitlab/gitlab-rails/production.log /var/log/gitlab/postgresql/current
</code></pre></div>
<p>All good!</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="backup" term="backup"/>
        <category label="gitlab" term="gitlab"/>
        <category label="postgresql" term="postgresql"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Integrate Algolia InstantSearch into a Vue Project]]></title>
        <id>https://pipo.blog/articles/20210830-algolia-vue-instantsearch</id>
        <link href="https://pipo.blog/articles/20210830-algolia-vue-instantsearch"/>
        <updated>2021-08-30T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Integrate Algolia InstantSearch into a Vue project using Nuxt/content.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/algolia-vue-instantsearch.png"></p>
                      <p>This TechBlog is based on <a href="https://vuejs.org/" rel="nofollow noopener noreferrer" target="_blank">Vue.js</a> &#x26; <a href="https://nuxtjs.org/" rel="nofollow noopener noreferrer" target="_blank">Nuxt.js</a>, using <a href="https://content.nuxtjs.org/" rel="nofollow noopener noreferrer" target="_blank">nuxt/content</a> as a Git-based headless CMS, <a href="https://tailwindcss.com/" rel="nofollow noopener noreferrer" target="_blank">Tailwind CSS</a> for styling. Today, we want to talk about full-text search integration. <a href="https://content.nuxtjs.org/" rel="nofollow noopener noreferrer" target="_blank">nuxt/content</a> actually has a built-in search which indexes your pages/articles and does full-text lookups that are super easy to integrate. But I had quite a bad experience with it, content search delivering wrong search results (that did not even contain the query string or anything similar at all), and making it hard to extract a snippet of surrounding words/sentences of the search results.</p>
<p><a href="https://www.algolia.com/" rel="nofollow noopener noreferrer" target="_blank">Algolia</a> offers a super powerful and flexible search with ready to use <a href="https://www.algolia.com/products/search-and-discovery/ui-component-libraries/" rel="nofollow noopener noreferrer" target="_blank">InstantSearch</a> UI components for <a href="https://reactjs.org/" rel="nofollow noopener noreferrer" target="_blank">React</a> and <a href="https://vuejs.org/" rel="nofollow noopener noreferrer" target="_blank">Vue</a>. It was quite easy to write my own <code>AlgoliaSearch</code> component for this blog, but Algolia's InstantSearch defaults were so horribly resource-hungry and it took me a while to figure out how to fine-tune this.</p>
<!--more-->
<h2 id="algoliasearch-vue-component"><a href="#algoliasearch-vue-component" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>AlgoliaSearch Vue Component</h2>
<p>In our <code>AlgoliaSearch</code> component, we're going to use the following <strong>Vue InstantSearch</strong> UI components:</p>
<ul>
<li><a href="https://www.algolia.com/doc/api-reference/widgets/instantsearch/vue/" rel="nofollow noopener noreferrer" target="_blank">ais-instant-search</a></li>
<li><a href="https://www.algolia.com/doc/api-reference/widgets/configure/vue/" rel="nofollow noopener noreferrer" target="_blank">ais-configure</a></li>
<li><a href="https://www.algolia.com/doc/api-reference/widgets/autocomplete/vue/" rel="nofollow noopener noreferrer" target="_blank">ais-autocomplete</a></li>
<li><a href="https://www.algolia.com/doc/api-reference/widgets/highlight/vue/" rel="nofollow noopener noreferrer" target="_blank">ais-highlight</a></li>
<li><a href="https://www.algolia.com/doc/api-reference/widgets/snippet/vue/" rel="nofollow noopener noreferrer" target="_blank">ais-snippet</a></li>
</ul>
<p>The following tutorials are great and give you in-depth instructions of how to set up such a search component:</p>
<ul>
<li><a href="https://dev.to/armiedema/create-a-keyboard-navigable-site-search-with-nuxt-content-and-aloglia-vue-instantsearch-2d6o" rel="nofollow noopener noreferrer" target="_blank">Create a keyboard navigable site search with Nuxt/Content and Aloglia vue-instantsearch</a></li>
<li><a href="https://www.zemna.net/articles/how-to-add-algolia-search-to-nuxtjs/" rel="nofollow noopener noreferrer" target="_blank">How to add Algolia Search to NuxtJS</a></li>
</ul>
<p>... so I try to keep it short here and just repeat the basic setup which is based on those articles. If you already have such a component set up, scroll down to the second part, how to fine-tune Algolia for a better search performance without the clutter and waste of resources.</p>
<blockquote>
<p>Here's the full code: <a href="https://gitlab.com/onlime/demo/nuxt-content-blog" rel="nofollow noopener noreferrer" target="_blank">demo/nuxt-content-blog</a></p>
<p>And here's the <a href="https://demo.pipo.blog/" rel="nofollow noopener noreferrer" target="_blank">DemoBlog</a> (a stripped-down version of this blog, so you better understand)</p>
</blockquote>
<p>The <a href="https://nuxtjs.org/" rel="nofollow noopener noreferrer" target="_blank">Nuxt</a> project initially was created as follows:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">yarn</span> create nuxt-app algolia-vue-instantsearch
? Project name: algolia-vue-instantsearch
? Programming language: JavaScript
? Package manager: Yarn
? UI framework: Tailwind CSS
? Nuxt.js modules: Content - Git-based headless CMS
? Linting tools: ESLint, Prettier
? Testing framework: None
? Rendering mode: Universal <span class="token punctuation">(</span>SSR / SSG<span class="token punctuation">)</span>
? Deployment target: Static <span class="token punctuation">(</span>Static/Jamstack hosting<span class="token punctuation">)</span>
? Development tools: jsconfig.json <span class="token punctuation">(</span>Recommended <span class="token keyword">for</span> VS Code <span class="token keyword">if</span> you're not using typescript<span class="token punctuation">)</span>
? Continuous integration: None
? Version control system: Git
</code></pre></div>
<p>Install the required packages:</p>
<ul>
<li><a href="https://www.npmjs.com/package/algoliasearch" rel="nofollow noopener noreferrer" target="_blank">algoliasearch</a>, <a href="https://github.com/algolia/vue-instantsearch" rel="nofollow noopener noreferrer" target="_blank">vue-instantsearch</a>: Algolia Vue InstantSearch</li>
<li><a href="https://www.npmjs.com/package/nuxt-content-algolia" rel="nofollow noopener noreferrer" target="_blank">nuxt-content-algolia</a>: Sync Nuxt content to Algolia index during <code>nuxt generate</code></li>
<li><a href="https://github.com/stiang/remove-markdown" rel="nofollow noopener noreferrer" target="_blank">remove-markdown</a>: Remove Markdown from content body text for Algolia search index.</li>
<li><a href="https://www.npmjs.com/package/v-click-outside" rel="nofollow noopener noreferrer" target="_blank">v-click-outside</a>: Vue directive to react on clicks outside an element, used to hide search results.</li>
</ul>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">yarn</span> <span class="token function">add</span> algoliasearch nuxt-content-algolia vue-instantsearch
$ <span class="token function">yarn</span> <span class="token function">add</span> remove-markdown v-click-outside
</code></pre></div>
<p>Create custom plugin in <code>plugins/vue-instantsearch.js</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">plugins/vue-instantsearch.js</span><pre class="line-numbers language-js"><code><span class="token keyword module">import</span> <span class="token imports"><span class="token maybe-class-name">Vue</span></span> <span class="token keyword module">from</span> <span class="token string">'vue'</span>
<span class="token keyword module">import</span> <span class="token imports"><span class="token maybe-class-name">InstantSearch</span></span> <span class="token keyword module">from</span> <span class="token string">'vue-instantsearch'</span>

<span class="token maybe-class-name">Vue</span><span class="token punctuation">.</span><span class="token method function property-access">use</span><span class="token punctuation">(</span><span class="token maybe-class-name">InstantSearch</span><span class="token punctuation">)</span>
</code></pre></div>
<p>Register custom plugin and set transpile build configuration:</p>
<div class="nuxt-content-highlight"><span class="filename">nuxt.config.js</span><pre class="line-numbers language-js"><code><span class="token keyword module">export</span> <span class="token keyword module">default</span> <span class="token punctuation">{</span>
    <span class="token comment">// ...</span>
    <span class="token literal-property property">plugins</span><span class="token operator">:</span> <span class="token punctuation">[</span>
        <span class="token string">'~/plugins/vue-instantsearch'</span>
    <span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token literal-property property">build</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token literal-property property">transpile</span><span class="token operator">:</span> <span class="token punctuation">[</span>
            <span class="token string">'vue-instantsearch'</span><span class="token punctuation">,</span>
            <span class="token string">'instantsearch.js/es'</span><span class="token punctuation">,</span>
        <span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token comment">// ...</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Configure <code>nuxt-content-algolia</code> to send index to Algolia during build (<code>nuxt generate</code>):</p>
<div class="nuxt-content-highlight"><span class="filename">nuxt.config.js</span><pre class="line-numbers language-js"><code><span class="token keyword module">export</span> <span class="token keyword module">default</span> <span class="token punctuation">{</span>
    <span class="token comment">// ...</span>
    <span class="token literal-property property">buildModules</span><span class="token operator">:</span> <span class="token punctuation">[</span>
        <span class="token string">'nuxt-content-algolia'</span><span class="token punctuation">,</span>
    <span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token literal-property property">nuxtContentAlgolia</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token literal-property property">appId</span><span class="token operator">:</span> process<span class="token punctuation">.</span><span class="token property-access">env</span><span class="token punctuation">.</span><span class="token constant">ALGOLIA_APP_ID</span><span class="token punctuation">,</span>
        <span class="token comment">// !IMPORTANT secret key should always be an environment variable</span>
        <span class="token comment">// this is not your search only key but the key that grants access to modify the index</span>
        <span class="token literal-property property">apiKey</span><span class="token operator">:</span> process<span class="token punctuation">.</span><span class="token property-access">env</span><span class="token punctuation">.</span><span class="token constant">ALGOLIA_API_KEY</span><span class="token punctuation">,</span>
        <span class="token comment">// relative to content directory - each path get's its own index</span>
        <span class="token literal-property property">paths</span><span class="token operator">:</span> <span class="token punctuation">[</span>
            <span class="token punctuation">{</span>
                <span class="token literal-property property">name</span><span class="token operator">:</span> <span class="token string">'articles'</span><span class="token punctuation">,</span>
                <span class="token literal-property property">index</span><span class="token operator">:</span> process<span class="token punctuation">.</span><span class="token property-access">env</span><span class="token punctuation">.</span><span class="token constant">ALGOLIA_INDEX</span> <span class="token operator">||</span> <span class="token string">'articles'</span><span class="token punctuation">,</span>
                <span class="token literal-property property">fields</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'title'</span><span class="token punctuation">,</span> <span class="token string">'description'</span><span class="token punctuation">,</span> <span class="token string">'bodyPlainText'</span><span class="token punctuation">,</span> <span class="token string">'tags'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
            <span class="token punctuation">}</span><span class="token punctuation">,</span>
        <span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token comment">// ...</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Markdown formatting/tags are not needed for search. So, we create a <code>bodyPlainText</code> using <a href="https://github.com/stiang/remove-markdown" rel="nofollow noopener noreferrer" target="_blank">remove-markdown</a> package, <code>nuxt.config.js</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">nuxt.config.js</span><pre class="line-numbers language-js"><code><span class="token keyword module">export</span> <span class="token keyword module">default</span> <span class="token punctuation">{</span>
    <span class="token comment">// ...</span>
    <span class="token literal-property property">hooks</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token string-property property">'content:file:beforeInsert'</span><span class="token operator">:</span> <span class="token punctuation">(</span><span class="token parameter"><span class="token dom variable">document</span></span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token punctuation">{</span>
            <span class="token keyword control-flow">if</span> <span class="token punctuation">(</span><span class="token dom variable">document</span><span class="token punctuation">.</span><span class="token property-access">extension</span> <span class="token operator">===</span> <span class="token string">'.md'</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
                <span class="token keyword">const</span> removeMd <span class="token operator">=</span> <span class="token function">require</span><span class="token punctuation">(</span><span class="token string">'remove-markdown'</span><span class="token punctuation">)</span>
                <span class="token dom variable">document</span><span class="token punctuation">.</span><span class="token property-access">bodyPlainText</span> <span class="token operator">=</span> <span class="token function">removeMd</span><span class="token punctuation">(</span><span class="token dom variable">document</span><span class="token punctuation">.</span><span class="token property-access">text</span><span class="token punctuation">)</span>
            <span class="token punctuation">}</span>
        <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token comment">// ...</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Create <code>.env</code> file and set required variables - put your Algolia app ID and search-only key there:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-ini"><code><span class="token key attr-name">ALGOLIA_INDEX</span><span class="token punctuation">=</span><span class="token value attr-value">dev_articles</span>
<span class="token key attr-name">ALGOLIA_APP_ID</span><span class="token punctuation">=</span><span class="token value attr-value">ABCDE12345</span>
<span class="token comment">#ALGOLIA_API_KEY=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span>
<span class="token key attr-name">ALGOLIA_SEARCH_ONLY_KEY</span><span class="token punctuation">=</span><span class="token value attr-value">xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx</span>
<span class="token key attr-name">ALGOLIA_HITS_PER_PAGE</span><span class="token punctuation">=</span><span class="token value attr-value">5</span>
<span class="token key attr-name">ALGOLIA_QUERY_BUFFER_TIME</span><span class="token punctuation">=</span><span class="token value attr-value">300</span>
</code></pre></div>
<blockquote>
<p><strong>NOTE:</strong> Copy <code>.env.example</code> from my <a href="https://gitlab.com/onlime/demo/nuxt-content-blog" rel="nofollow noopener noreferrer" target="_blank">demo/nuxt-content-blog</a> to <code>.env</code> to start with.</p>
<p><strong>WARNING:</strong> Make sure, you never store <code>ALGOLIA_API_KEY</code> (the private API key) in your git repo. Only use it in your dev environment's <code>.env</code> and NEVER put it in your production <code>.env</code>. If you deploy to your production host with GitLab CI/CD, you can easily set <code>ALGOLIA_API_KEY</code> as an environment variable only, as this is only needed by <a href="https://www.npmjs.com/package/nuxt-content-algolia" rel="nofollow noopener noreferrer" target="_blank">nuxt-content-algolia</a> in build step on <code>nuxt generate</code>.</p>
<p>I also recommend setting up 2 separate indices at Algolia (same account), <code>dev_articles</code> for development and <code>prod_articles</code> for production. So you would put <code>ALGOLIA_INDEX=prod_articles</code> into your production <code>.env</code>.</p>
</blockquote>
<p>The skeleton of <code>AlgoliaSearch</code> Vue component should then look like this (find full component here: <a href="https://gitlab.com/onlime/demo/nuxt-content-blog/-/blob/main/components/AlgoliaSearch.vue" rel="nofollow noopener noreferrer" target="_blank"><code>AlgoliaSearch.vue</code></a>):</p>
<div class="nuxt-content-highlight"><span class="filename">components/AlgoliaSearch.vue</span><pre class="line-numbers language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>template</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>ais-instant-search</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>ais-configure</span> <span class="token punctuation">/></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>ais-autocomplete</span><span class="token punctuation">></span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>div</span> <span class="token attr-name">slot-scope</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>{ currentRefinement, indices, refine }<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
                <span class="token comment">&#x3C;!-- search input field --></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>input</span> <span class="token attr-name">@input</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>refine($event.currentTarget.value)<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
                <span class="token comment">&#x3C;!-- search results --></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>div</span> <span class="token attr-name">v-if</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>currentRefinement.length<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>div</span> <span class="token attr-name">v-for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>section in indices<span class="token punctuation">"</span></span> <span class="token attr-name">:key</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>section.objectID<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
                        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>NuxtLink</span> <span class="token attr-name">v-for</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>(hit, index) in section.hits<span class="token punctuation">"</span></span> <span class="token attr-name">:key</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hit.objectID<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
                            <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>ais-highlight</span> <span class="token attr-name">attribute</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>title<span class="token punctuation">"</span></span> <span class="token attr-name">:hit</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hit<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
                            <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>ais-snippet</span> <span class="token attr-name">attribute</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>bodyPlainText<span class="token punctuation">"</span></span> <span class="token attr-name">:hit</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>hit<span class="token punctuation">"</span></span> <span class="token punctuation">/></span></span>
                        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>NuxtLink</span><span class="token punctuation">></span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>div</span><span class="token punctuation">></span></span>
                    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>ais-powered-by</span> <span class="token punctuation">/></span></span>
                <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>div</span><span class="token punctuation">></span></span>
            <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>div</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>ais-autocomplete</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>ais-instant-search</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>template</span><span class="token punctuation">></span></span>
</code></pre></div>
<p>Finally, insert <code>AlgoliaSearch</code> Vue component in your layout:</p>
<div class="nuxt-content-highlight"><span class="filename">layouts/default.vue</span><pre class="line-numbers language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>template</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>header</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>AlgoliaSearch</span> <span class="token punctuation">/></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>header</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>template</span><span class="token punctuation">></span></span>
</code></pre></div>
<p>full code here: <a href="https://gitlab.com/onlime/demo/nuxt-content-blog" rel="nofollow noopener noreferrer" target="_blank">demo/nuxt-content-blog</a></p>
<h2 id="instantsearch-fine-tuning"><a href="#instantsearch-fine-tuning" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>InstantSearch fine-tuning</h2>
<p>If you copied the Search component from the <a href="https://dev.to/armiedema/create-a-keyboard-navigable-site-search-with-nuxt-content-and-aloglia-vue-instantsearch-2d6o" rel="nofollow noopener noreferrer" target="_blank">original article</a>, and have signed up for an Algolia Search account, the default search experience is quite bad. Not performance-wise – Algolia keeps its promise and delivers search responses in &#x3C; 30ms. But look at this waste of resources! ...</p>
<ul>
<li>By default, the autocomplete search field fires a <code>POST</code> request to Algolia's servers <strong>on every single keystroke</strong>!</li>
<li>By default, InstantSearch sends an <strong>initial request to Algolia’s servers with an empty query</strong> (see <a href="https://www.algolia.com/doc/guides/building-search-ui/going-further/conditional-requests/vue/" rel="nofollow noopener noreferrer" target="_blank">Conditional Requests</a>). Algolia explains it with: "This connection helps speed up later requests." – But for a regular use-case, this will just increase transferred data over the network and does not improve search speed at all (the initial response just contains the last x articles, anyway).</li>
<li>By default, InstantSearch <strong>responds with all attributes</strong>, also returning full article content which might be several KB per article. As Algolia even duplicates the whole content in its JSON response for highlighting, it will even <strong>double the transferred data</strong>.</li>
<li>If you accidentally wrap the <a href="https://www.algolia.com/doc/api-reference/widgets/configure/vue/" rel="nofollow noopener noreferrer" target="_blank"><code>ais-configure</code> tag</a> around your <a href="https://www.algolia.com/doc/api-reference/widgets/autocomplete/vue/" rel="nofollow noopener noreferrer" target="_blank">ais-autocomplete</a> component (which works perfectly fine and was even recommended by the <a href="https://dev.to/armiedema/create-a-keyboard-navigable-site-search-with-nuxt-content-and-aloglia-vue-instantsearch-2d6o" rel="nofollow noopener noreferrer" target="_blank">original article</a>), this would blow up your initial <code>POST</code> request again, triggering 2 requests instead of just 1.</li>
<li>If you add your <code>AlgoliaSearch</code> component at multiple place, e.g. in your regular screen header and in your mobile menu, that would double the initial requests to Algolia again.</li>
</ul>
<p>I managed to fire 4 <code>POST</code> requests to Algolia's servers on initial page load without even touching the search input field. Any consecutive search query fired several requests, one for each keystroke. Like this, I managed to already use up half of my free 10'000 requests/mo in the first two days of integrating the search into this blog, without even publishing it yet. And when I checked Algolia's responses in the developer console, I noticed each response is delivering over 10KB of JSON data. <strong>This is insane!</strong></p>
<blockquote>
<p>Please, Algolia, stop beeing so greedy and provide us with some sane defaults that are not so resource-hungry! Think about the climate and not just about monetizing your services!</p>
</blockquote>
<p>I have managed to fine-tune it to the following:</p>
<ul>
<li><strong>No more initial request getting fired to Algolia.</strong> If the visitor does not submit a search query, no request!</li>
<li><strong>Autocomplete search field is now throttling your keystrokes.</strong> It will just fire a search query after 300ms, so if you type fast enough, there will be only a single search request fired to Algolia's servers!</li>
<li>Full-text content <code>bodyPlainText</code> is no longer delivered in Algolia's search response.</li>
<li>Full-text content <code>bodyPlainText</code> is no longer duplicated for highlighting (which anyway we don't need) in Algolia's search response.</li>
</ul>
<p>That's how I did:</p>
<h3 id="avoid-initial-request"><a href="#avoid-initial-request" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Avoid Initial Request</h3>
<p>Algolia obtrusively makes you send an initial <code>POST</code> request with response data you will probably (in most use cases) never need. There is no way to turn this off by configuration, there is only this hint here: <a href="https://www.algolia.com/doc/guides/building-search-ui/going-further/conditional-requests/vue/#detecting-empty-search-requests" rel="nofollow noopener noreferrer" target="_blank">Conditional Requests > Detecting empty search results</a> and for a real workaround you would need to deeply dig into <a href="https://github.com/algolia/doc-code-samples/tree/master/Vue%20InstantSearch/conditional-request" rel="nofollow noopener noreferrer" target="_blank">this sample code</a>.</p>
<p>So, this is my solution:</p>
<div class="nuxt-content-highlight"><span class="filename">AlgoliaSearch.vue</span><pre class="line-numbers language-js"><code><span class="token keyword module">export</span> <span class="token keyword module">default</span> <span class="token punctuation">{</span>
    <span class="token literal-property property">computed</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token function">searchClient</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
            <span class="token keyword">const</span> algoliaClient <span class="token operator">=</span> <span class="token function">algoliasearch</span><span class="token punctuation">(</span>
                <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token property-access">$config</span><span class="token punctuation">.</span><span class="token property-access">algoliaAppId</span><span class="token punctuation">,</span>
                <span class="token keyword">this</span><span class="token punctuation">.</span><span class="token property-access">$config</span><span class="token punctuation">.</span><span class="token property-access">algoliaSearchOnlyKey</span>
            <span class="token punctuation">)</span>
            <span class="token keyword control-flow">return</span> <span class="token punctuation">{</span>
                <span class="token spread operator">...</span>algoliaClient<span class="token punctuation">,</span>
                <span class="token function">search</span><span class="token punctuation">(</span><span class="token parameter">requests</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
                    <span class="token keyword control-flow">if</span> <span class="token punctuation">(</span>requests<span class="token punctuation">.</span><span class="token method function property-access">every</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter"><span class="token punctuation">{</span> params <span class="token punctuation">}</span></span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token operator">!</span>params<span class="token punctuation">.</span><span class="token property-access">query</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
                        <span class="token keyword control-flow">return</span> <span class="token known-class-name class-name">Promise</span><span class="token punctuation">.</span><span class="token method function property-access">resolve</span><span class="token punctuation">(</span><span class="token punctuation">{</span>
                            <span class="token literal-property property">results</span><span class="token operator">:</span> requests<span class="token punctuation">.</span><span class="token method function property-access">map</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token punctuation">(</span><span class="token punctuation">{</span>
                                <span class="token literal-property property">hits</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
                                <span class="token literal-property property">nbHits</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span>
                                <span class="token literal-property property">processingTimeMS</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span>
                            <span class="token punctuation">}</span><span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">,</span>
                        <span class="token punctuation">}</span><span class="token punctuation">)</span>
                    <span class="token punctuation">}</span>
                    <span class="token keyword control-flow">return</span> algoliaClient<span class="token punctuation">.</span><span class="token method function property-access">search</span><span class="token punctuation">(</span>requests<span class="token punctuation">)</span>
                <span class="token punctuation">}</span><span class="token punctuation">,</span>
            <span class="token punctuation">}</span>
        <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>I put this into a computed function, as we want to have <code>process.env</code> or <code>this.$config</code> available for <code>ALGOLIA_APP_ID</code> / <code>ALGOLIA_SEARCH_ONLY_KEY</code> env var lookups.</p>
<p>You may then pass <code>searchClient</code> to the <a href="https://www.algolia.com/doc/api-reference/widgets/instantsearch/vue/" rel="nofollow noopener noreferrer" target="_blank">ais-instant-search</a> component:</p>
<div class="nuxt-content-highlight"><span class="filename">AlgoliaSearch.vue</span><pre class="line-numbers language-html"><code><span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>template</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>ais-instant-search</span>
        <span class="token attr-name">:search-client</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>searchClient<span class="token punctuation">"</span></span>
        <span class="token attr-name">:index-name</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>$config.algoliaIndex<span class="token punctuation">"</span></span>
        <span class="token attr-name">:search-function</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>searchFunction<span class="token punctuation">"</span></span>
    <span class="token punctuation">></span></span>
    ...
</code></pre></div>
<h3 id="throttle-search-submission"><a href="#throttle-search-submission" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Throttle Search Submission</h3>
<p>In above snippet, you might have noticed I have provided a <code>searchFunction</code> to the <a href="https://www.algolia.com/doc/api-reference/widgets/instantsearch/vue/" rel="nofollow noopener noreferrer" target="_blank">ais-instant-search</a> component. This is needed to debounce the search submission so that we're not going to fire a search request on every keystroke.</p>
<p>Please read <a href="https://www.algolia.com/doc/api-reference/widgets/instantsearch/vue/#widget-param-search-function" rel="nofollow noopener noreferrer" target="_blank">ais-instant-search prop <code>search-function</code></a> API documentation. This is my solution:</p>
<div class="nuxt-content-highlight"><span class="filename">AlgoliaSearch.vue</span><pre class="line-numbers language-json"><code>export default <span class="token punctuation">{</span>
    methods<span class="token operator">:</span> <span class="token punctuation">{</span>
        searchFunction(helper) <span class="token punctuation">{</span>
            if (helper.state.query) <span class="token punctuation">{</span>
                <span class="token comment">// debounce search queries, so that an Algolia search request is not triggered </span>
                <span class="token comment">// if another search query has overwritten the query during the same 300ms</span>
                this.$debounce(<span class="token number">300</span><span class="token punctuation">,</span> 'search').then(() => <span class="token punctuation">{</span>
                    helper.search()
                    <span class="token comment">// ensure that search results are always shown, even if they were hidden by previous navigation to</span>
                    <span class="token comment">// same route that was already active</span>
                    this.showResults = <span class="token boolean">true</span>
                <span class="token punctuation">}</span>)
            <span class="token punctuation">}</span> else <span class="token punctuation">{</span>
                <span class="token comment">// on empty search query, fire search immediately (will send local response, see computed searchClient())</span>
                <span class="token comment">// console.log('Algolia search called with query: ' + helper.state.query)</span>
                helper.search()
            <span class="token punctuation">}</span>
        <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div>
<blockquote>
<p><strong>UPDATE 2021-09-17:</strong> I have updated above <code>searchFunction</code> to use my global debounce function which is injected in Nuxt plugin <code>plugins/helpers.js</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">plugins/helpers.js</span><pre class="line-numbers language-js"><code><span class="token keyword">const</span> debounceStack <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token punctuation">]</span>
<span class="token keyword">const</span> debounce <span class="token operator">=</span> <span class="token punctuation">(</span>delay<span class="token punctuation">,</span> key <span class="token operator">=</span> <span class="token string">'global'</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token punctuation">{</span>
    <span class="token keyword control-flow">return</span> <span class="token keyword">new</span> <span class="token class-name">Promise</span><span class="token punctuation">(</span><span class="token punctuation">(</span><span class="token parameter">resolve</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token punctuation">{</span>
        <span class="token function">clearTimeout</span><span class="token punctuation">(</span>debounceStack<span class="token punctuation">[</span>key<span class="token punctuation">]</span><span class="token punctuation">)</span>
        debounceStack<span class="token punctuation">[</span>key<span class="token punctuation">]</span> <span class="token operator">=</span> <span class="token function">setTimeout</span><span class="token punctuation">(</span>resolve<span class="token punctuation">,</span> delay<span class="token punctuation">)</span>
    <span class="token punctuation">}</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>

<span class="token keyword module">export</span> <span class="token keyword module">default</span> <span class="token punctuation">(</span><span class="token parameter">_context<span class="token punctuation">,</span> inject</span><span class="token punctuation">)</span> <span class="token arrow operator">=></span> <span class="token punctuation">{</span>
    <span class="token function">inject</span><span class="token punctuation">(</span><span class="token string">'debounce'</span><span class="token punctuation">,</span> debounce<span class="token punctuation">)</span>
<span class="token punctuation">}</span>
</code></pre></div>
</blockquote>
<p>This is a tech blog, so I guess a regular visitor types fast enough (you're a dev that types fast as hell, right?) to barely ever wait more than 300ms between two keystrokes. The search will be delayed by 300ms, but we can live with that. If you're on a paid Algolia plan and have the money for a lot of extra requests, go ahead and reduce that delay. In my <a href="https://gitlab.com/onlime/demo/nuxt-content-blog" rel="nofollow noopener noreferrer" target="_blank">demo/nuxt-content-blog</a> codebase, you can tune this by environment variable in <code>.env</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">.env</span><pre class="line-numbers language-ini"><code><span class="token key attr-name">ALGOLIA_QUERY_BUFFER_TIME</span><span class="token punctuation">=</span><span class="token value attr-value">300</span>
</code></pre></div>
<p>You can play around with the search field in my <a href="https://demo.pipo.blog/" rel="nofollow noopener noreferrer" target="_blank">DemoBlog</a> – but make sure you don't type too slow and use up all my free credits at Algolia!</p>
<h3 id="reduce-response-data"><a href="#reduce-response-data" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Reduce Response Data</h3>
<p>Again, Algolia by default delivers way too much data in every search response JSON. If you did not fine-tune this in Algolia's dashboard, you would get the full article <code>bodyPlainText</code> content duplicated for every article in your response. We don't need those fields:</p>
<div class="nuxt-content-highlight"><span class="filename">JSON response</span><pre class="line-numbers language-json"><code><span class="token punctuation">{</span>
    <span class="token property">"results"</span><span class="token operator">:</span> <span class="token punctuation">[</span>
        <span class="token punctuation">{</span>
            <span class="token property">"hits"</span><span class="token operator">:</span> <span class="token punctuation">[</span>
                <span class="token punctuation">{</span>
                    <span class="token property">"title"</span><span class="token operator">:</span> <span class="token string">"..."</span><span class="token punctuation">,</span>
                    <span class="token property">"description"</span><span class="token operator">:</span> <span class="token string">"..."</span><span class="token punctuation">,</span>
                    <span class="token property">"bodyPlainText"</span><span class="token operator">:</span> <span class="token string">"**REMOVE_THIS**"</span><span class="token punctuation">,</span>
                    <span class="token property">"tags"</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">"..."</span><span class="token punctuation">,</span> <span class="token string">"..."</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
                    <span class="token property">"objectID"</span><span class="token operator">:</span> <span class="token string">"..."</span><span class="token punctuation">,</span>
                    <span class="token property">"_highlightResult"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
                        <span class="token property">"bodyPlainText"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
                            <span class="token property">"value"</span><span class="token operator">:</span> <span class="token string">"**REMOVE_THIS**"</span>
                        <span class="token punctuation">}</span>
                    <span class="token punctuation">}</span>
                <span class="token punctuation">}</span>
            <span class="token punctuation">]</span>
        <span class="token punctuation">}</span>
    <span class="token punctuation">]</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>AFAIK, there is no way to configure this client-side as request param. You need to configure this in Algolia dashboard for each (<code>dev_articles</code>, <code>prod_articles</code>) index:</p>
<ul>
<li>Relevance Essentials > <strong>Searchable attributes</strong>: <code>title, description, bodyPlainText, tags</code> (here, you must include <code>bodyPlainText</code>)</li>
<li>Search Behaviour > <strong>Retrieved attributes</strong>: <code>description, tags, title</code> (remove default <code>*</code>)</li>
<li>Pagination and Display > Highlighting > <strong>Attributes to highlight</strong>: <code>description, title</code> (don't include <code>bodyPlainText</code>)</li>
</ul>
<p>Your JSON responses will then be below 1KB, while previously they could easily grow over 10KB for just 5 search results, depending on your article sizes.</p>
<h3 id="other-algolia-settings"><a href="#other-algolia-settings" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Other Algolia Settings</h3>
<p>I also recommend to enable the following in Algolia Dashboard:</p>
<ul>
<li>Search Behaviour > <strong>Advanced Syntax</strong></li>
</ul>
<p>That provides you: exact matching of quoted expressions and exclusion of words preceded by a “-“ sign, see <a href="https://www.algolia.com/doc/api-reference/api-parameters/advancedSyntax/" rel="nofollow noopener noreferrer" target="_blank"><code>advancedSyntax</code> API parameter</a></p>
<p>That's it. Please let me know if you have further ideas for improvement.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="frontend" term="frontend"/>
        <category label="vue.js" term="vue.js"/>
        <category label="nuxt.js" term="nuxt.js"/>
        <category label="js" term="js"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Automated Mail Client Configuration with email-autoconf]]></title>
        <id>https://pipo.blog/articles/20210826-email-autoconf</id>
        <link href="https://pipo.blog/articles/20210826-email-autoconf"/>
        <updated>2021-08-26T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Automated mail client configuration using email-autoconf as automx2 replacement.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/email-autoconf.png"></p>
                      <p>How nice would it be, if every email client could be auto-configured and the user would just need to enter his credentials? Entering his email address plus password, hit enter, done. How come nowadays people still need to care about incoming IMAP and outgoing SMTP servers, encryption (SSL/TLS vs. STARTTLS protocols), ports (for a regular end user this is just a number without any meaning, right?), and maybe even wondering what was the username if that differs from the actual email address?</p>
<p>You may tell me: <em>Hey, we're in 2021, email is so old-school!</em> Or you may tell me: <em>Oh, don't you know there is a thing called auto-configuration?</em> – Unfortunately, both statements are wrong. Email today is still widely used for serious communication (and yes, you're right, it should have been replaced long time ago by something better! But it didn't.), and there is such a thing called auto-configuration, but there is just no real standardized protocol that works for every mail client out there. There was no real development in that field during the last 10+ years, sadly.</p>
<p>So, let me present that small Python project <a href="https://www.wittwer-it.ch/" rel="nofollow noopener noreferrer" target="_blank">Martin</a> and I have been developing for <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime GmbH</a> back in Feb 2020 and which I have now finally published on GitLab.com: <strong><a href="https://gitlab.com/onlime/email-autoconf" rel="nofollow noopener noreferrer" target="_blank">onlime/email-autoconf</a></strong></p>
<!--more-->
<h2 id="history-automx-vs-email-autoconf"><a href="#history-automx-vs-email-autoconf" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>History: automx vs. email-autoconf</h2>
<p>For years, I have been using <a href="https://automx.org/" rel="nofollow noopener noreferrer" target="_blank">automx</a> (version 1) by the German <a href="https://sys4.de" rel="nofollow noopener noreferrer" target="_blank">Sys4 AG</a> to serve our customers with auto-configuration for common mail clients. But that project was a pain, as it was badly maintained and was still based on ancient Python 2 at that time. It was not available as Debian package, and it required some patching to get it working as expected. Finally, back in Dec 2019, Sys4 has published a completely new version <a href="https://github.com/rseichter/automx2" rel="nofollow noopener noreferrer" target="_blank">automx2</a> which was actually a full rewrite and had pretty much nothing in common with the previous product.</p>
<p>We tried to get automx2 running for our environment with the simple requirement of doing database (MySQL) based lookups of email-to-username mappings, as some legacy customers of <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime GmbH</a> still have email accounts where the username does not match the actual email address. That thing was so easy to configure in automx(1) but got a real pain in automx2 – It even looked like they have just forgotten about it. Overall, automx2 was so over-engineered and hard to install that it did not make our lives any easier. So, we just decided to write our own auto-configuration tool from scratch.</p>
<blockquote>
<p>Please don't take me wrong.  <a href="https://github.com/rseichter/automx2" rel="nofollow noopener noreferrer" target="_blank">automx2</a> seems to have matured in the meantime, is much better documented, and may be easy to deploy. I didn't re-evaluate it since then, as we just didn't need it anymore.</p>
</blockquote>
<p>And before you're getting too excited...</p>
<p>Deployment of <a href="https://gitlab.com/onlime/email-autoconf" rel="nofollow noopener noreferrer" target="_blank">email-autoconf</a> currently is not that straight-forwarded yet, as up to now we were just using it for our mail services at <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime</a> and it has just been published (2021-08-26). This is fresh stuff, even though running absolutely wonderful (no single hang-up or performance issue!) since early 2020. I plan to publish an <a href="https://galaxy.ansible.com/" rel="nofollow noopener noreferrer" target="_blank">Ansible Galaxy</a> role soon to make deployment easier.</p>
<h2 id="requirements"><a href="#requirements" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Requirements</h2>
<p>Requirements for <a href="https://gitlab.com/onlime/email-autoconf" rel="nofollow noopener noreferrer" target="_blank">email-autoconf</a> were quite simple. It should support the following 3 protocols out of the box:</p>
<ul>
<li>Mozilla <a href="https://wiki.mozilla.org/Thunderbird:Autoconfiguration" rel="nofollow noopener noreferrer" target="_blank">Autoconfig</a> (Thunderbird)</li>
<li>Microsoft <a href="https://docs.microsoft.com/en-us/exchange/architecture/client-access/autodiscover" rel="nofollow noopener noreferrer" target="_blank">Autodiscover</a> (Outlook)</li>
<li>Apple <a href="https://support.apple.com/de-de/guide/profile-manager/pmdbd71ebc9/mac" rel="nofollow noopener noreferrer" target="_blank">Mobileconfig</a> (Apple Mail, iOS)</li>
</ul>
<p>(... in the order of my preference. Why couldn't they all just adapt Mozilla's protocol which is super-duper simple?!)</p>
<p>It should also support the following:</p>
<ul>
<li>Option to run it without a database backend</li>
<li>Option with a database backend in which it can lookup usernames and display names</li>
<li>Let it run in the following flavors: Standalone on standard HTTPS/443 port, or with a reverse proxy (Apache/Nginx) in front, running on an internal port or socket, while the SSL-cert is served by the proxy.</li>
<li>Easy and flexible to configure</li>
<li>Serving high loads, providing great performance</li>
</ul>
<p>We chose <a href="https://flask.palletsprojects.com" rel="nofollow noopener noreferrer" target="_blank">Flask</a> as <a href="https://www.python.org/" rel="nofollow noopener noreferrer" target="_blank">Python 3</a> microframework running on <a href="https://uwsgi-docs.readthedocs.io" rel="nofollow noopener noreferrer" target="_blank">uWSGI</a> as application server. You could use any other application server like <a href="https://gunicorn.org/" rel="nofollow noopener noreferrer" target="_blank">Gunicorn</a>, but uWSGI is the only one that has been tested and documented by us.</p>
<h2 id="installation"><a href="#installation" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Installation</h2>
<blockquote>
<p><strong>NOTE:</strong> In below result examples, we use <code>example.org</code> as company infrastructure domain where you run <strong>email-autoconf</strong>, and <code>example.com</code> as customer domain which is hosted by you.</p>
</blockquote>
<p>First, install some required packages, e.g. on Debian Linux:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token comment"># Python3 packages for venv</span>
$ <span class="token function">apt</span> <span class="token function">install</span> python3-venv python3-pip python3-dev
<span class="token comment"># uWSGI requirements</span>
$ <span class="token function">apt</span> <span class="token function">install</span> libssl-dev libpcre3-dev
</code></pre></div>
<p>Clone the repo, set up venv, and install required Python modules:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">git</span> clone https://gitlab.com/onlime/email-autoconf.git
$ <span class="token builtin class-name">cd</span> email-autoconf
$ python3 <span class="token parameter variable">-m</span> venv venv
$ <span class="token builtin class-name">.</span> venv/bin/activate
$ pip <span class="token function">install</span> <span class="token parameter variable">-r</span> requirements.txt
</code></pre></div>
<p>Copy the example config from <code>instance/settings.py_example</code> to <code>instance/settings.py</code> and edit it to match your needs:</p>
<div class="nuxt-content-highlight"><span class="filename">instance/settings.py</span><pre class="line-numbers language-python"><code>DB_TYPE <span class="token operator">=</span> <span class="token string">'mysql'</span>
DB_HOST <span class="token operator">=</span> <span class="token string">'127.0.0.1'</span>
DB_PORT <span class="token operator">=</span> <span class="token string">'3306'</span>
DB_NAME <span class="token operator">=</span> <span class="token string">'email-autoconf'</span>
DB_USER <span class="token operator">=</span> <span class="token string">'email-autoconf'</span>
DB_PASSWORD <span class="token operator">=</span> <span class="token string">'************'</span>

SELECT_USERNAME <span class="token operator">=</span> <span class="token string">"SELECT `dest` FROM `mailmap` WHERE `address` = '%a'"</span>
SELECT_DISPLAY_NAME <span class="token operator">=</span> <span class="token string">"SELECT `display_name` FROM `mailaccounts` WHERE `username` = '%u'"</span>

IMAP_SERVER_HOSTNAME <span class="token operator">=</span> <span class="token string">'imap.example.org'</span>
POP3_SERVER_HOSTNAME <span class="token operator">=</span> <span class="token string">'pop.example.org'</span>
SMTP_SERVER_HOSTNAME <span class="token operator">=</span> <span class="token string">'smtp.example.org'</span>

EMAIL_PROVIDER <span class="token operator">=</span> <span class="token string">'example.org'</span>
</code></pre></div>
<p>For all possible options, consult <em>Configuration</em> section below.</p>
<blockquote>
<p>Set <code>DB_TYPE = 'none'</code> if you don't need any specific email-to-username lookups. This works in an environment where every mail account uses its email address as username.</p>
</blockquote>
<p>Now choose one of the 3 available uWSGI instance configurations:</p>
<ul>
<li><code>uwsgi-http-instance.yml_example</code>: Testing instance / HTTP port 8080</li>
<li><code>uwsgi-https-instance.yml_example</code>: HTTPS port 8443</li>
<li><code>uwsgi-socket-instance.yml_example</code>: Socket port 3032</li>
</ul>
<p>Use those configurations as examples and deploy the one that fits your need. We recommend running it as a socket on port 3032 with <code>uwsgi-socket-instance.yml</code> instance config and set up an Apache/Nginx reverse proxy (see "Reverse Proxy" section below) in front of it for proper HTTPS support and optimal performance.</p>
<p>For testing, copy <code>uwsgi-http-instance.yml_example</code> to <code>uwsgi-http-instance.yml</code> and run the instance using <a href="https://uwsgi-docs.readthedocs.io/" rel="nofollow noopener noreferrer" target="_blank">uWSGI</a> as application server:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ uwsgi <span class="token parameter variable">--yaml</span> uwsgi-http.yml <span class="token parameter variable">--yaml</span> uwsgi-http-instance.yml
</code></pre></div>
<p>For basic testing, use <code>curl</code> e.g. to retrieve the autoconfig Mozilla scheme:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token function">curl</span> localhost:8080/mail/config-v1.1.xml?emailaddress<span class="token operator">=</span>demo@example.com
</code></pre></div>
<p>For in-depth testing, use the provided <code>scripts/email_autoconf_test.py</code>, see <em>Testing</em> section below.</p>
<h2 id="configuration"><a href="#configuration" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Configuration</h2>
<p>It is possible to configure email-autoconf within the file <code>instance/settings.py</code> or with environment variables. See <code>instance/settings.py_example</code> for an example configuration.</p>
<p>Environment variables could be handed over if you add them in front of the <code>uwsgi</code> command, like this:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token assign-left variable">SMTP_SERVER</span><span class="token operator">=</span>smtp.example.org uwsgi <span class="token parameter variable">--yaml</span> uwsgi-http.yml <span class="token parameter variable">--yaml</span> uwsgi-http-instance.yml
</code></pre></div>
<p>Settings for the uWSGI instance could be done in a <code>.yml</code> file which you then use in the start command. We recommend consulting the documentation on <a href="https://uwsgi-docs.readthedocs.io/en/latest/Configuration.html" rel="nofollow noopener noreferrer" target="_blank">Configuring uWSGI</a> for more details about the uWSGI configuration.</p>
<p>For a full list of config parameters, consult: <a href="https://gitlab.com/onlime/email-autoconf/-/blob/main/docs/documentation.md#parameters" rel="nofollow noopener noreferrer" target="_blank">documentation.md</a></p>
<h2 id="systemd-service"><a href="#systemd-service" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Systemd Service</h2>
<p>We recommend running email-autoconf as a Systemd service. The following setup has been proved working on a Debian Linux (tested on Debian 10 Buster).</p>
<blockquote>
<p><strong>IMPORTANT: Don’t run as <code>root</code>!</strong></p>
<p>If you use a port number greater than 1024 (I suggest port 3032), the application does not require super user privileges when running. It also does not need to be installed as root. It is recommended that you create a user account specifically for  email-autoconf, but other unprivileged users will do as well.</p>
</blockquote>
<p>Create <code>/etc/systemd/system/email-autoconf.service</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">email-autoconf.service</span><pre class="line-numbers language-ini"><code><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Unit</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">Description</span><span class="token punctuation">=</span><span class="token value attr-value">uWSGI instance to serve email-autoconf</span>
<span class="token key attr-name">After</span><span class="token punctuation">=</span><span class="token value attr-value">network.target</span>

<span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Service</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">Type</span><span class="token punctuation">=</span><span class="token value attr-value">simple</span>
<span class="token key attr-name">User</span><span class="token punctuation">=</span><span class="token value attr-value">email-autoconf</span>
<span class="token key attr-name">Group</span><span class="token punctuation">=</span><span class="token value attr-value">email-autoconf</span>
<span class="token key attr-name">WorkingDirectory</span><span class="token punctuation">=</span><span class="token value attr-value">/var/www/email-autoconf</span>
<span class="token key attr-name">ExecStart</span><span class="token punctuation">=</span><span class="token value attr-value">/var/www/email-autoconf/venv/bin/uwsgi --yaml uwsgi-http.yml --yaml uwsgi-socket-instance.yml</span>
<span class="token key attr-name">Restart</span><span class="token punctuation">=</span><span class="token value attr-value">on-failure</span>

<span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Install</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">WantedBy</span><span class="token punctuation">=</span><span class="token value attr-value">multi-user.target</span>
</code></pre></div>
<p>After creating this new Systemd service unit, run:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ systemctl daemon-reload
</code></pre></div>
<p>Then, enable/start the service:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ systemctl <span class="token builtin class-name">enable</span> email-autoconf.service
$ systemctl start email-autoconf.service
$ systemctl status email-autoconf.service
</code></pre></div>
<p>email-autoconf should now in socket mode on port 3032 (as we have provided<code>uwsgi-socket-instance.yml</code> instance config in Systemd <code>ExecStart</code> command).</p>
<h2 id="dns-configuration"><a href="#dns-configuration" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>DNS Configuration</h2>
<p>On your company's infrastructure DNS zone (the domain you usually put into <code>EMAIL_PROVIDER</code> in your instance config), you should configure the following DNS resource records:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>email-autoconf  IN  A   X.X.X.X. ; IP of the server running email-autoconf

maildiscovery   IN  CNAME   email-autoconf.example.org.
autoconfig  IN  CNAME   maildiscovery.example.org.
autodiscover    IN  CNAME   maildiscovery.example.org.
</code></pre></div>
<p>On any customer domain, the following records should be configured:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>autoconfig  300 IN  CNAME   maildiscovery.example.org.
_autodiscover._tcp  300 IN  SRV 0 0 443 maildiscovery.example.org.
</code></pre></div>
<h2 id="reverse-proxy"><a href="#reverse-proxy" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Reverse Proxy</h2>
<p>To simplify SSL handling and protect the uWSGI server, we strongly recommend using some sort of reverse proxy in front of email-autoconf. Below you can find config examples for Apache and Nginx.</p>
<blockquote>
<p>NOTE: Below examples are based on the recommended <strong><code>uwsgi-socket-instance.yml</code> uWSGI instance configuration</strong> for email-autoconf, running it as a <strong>socket on port 3032</strong>.</p>
</blockquote>
<h3 id="apache"><a href="#apache" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Apache</h3>
<p>In Apache, the modules <a href="https://httpd.apache.org/docs/2.4/mod/mod_proxy.html" rel="nofollow noopener noreferrer" target="_blank"><code>proxy</code></a> and <a href="https://httpd.apache.org/docs/2.4/mod/mod_proxy_uwsgi.html" rel="nofollow noopener noreferrer" target="_blank"><code>proxy_uwsgi</code></a> has to be enabled, while <code>proxy</code> should already be enabled by default.</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ a2enmod proxy_uwsgi
$ systemctl restart apache2
</code></pre></div>
<p>Apache vitual hosts to proxy requests to the email-autoconf uWSGI server could look like this:</p>
<div class="nuxt-content-highlight"><span class="filename">email-autoconf.conf</span><pre class="line-numbers language-apacheconf"><code><span class="token directive-block tag"><span class="token directive-block tag"><span class="token punctuation">&#x3C;</span>VirtualHost</span><span class="token directive-block-parameter attr-value"> *<span class="token punctuation">:</span>80</span><span class="token punctuation">></span></span>
    <span class="token directive-inline property">ServerName</span> autoconfig.example.org
    <span class="token directive-inline property">ServerAlias</span> autoconfig.*
    <span class="token directive-inline property">ServerAlias</span> maildiscovery.example.org maildiscovery.*
    <span class="token directive-inline property">ServerAlias</span> autodiscover.example.org autodiscover.*
    <span class="token directive-inline property">ErrorLog</span> <span class="token variable">${APACHE_LOG_DIR}</span>/email_autoconf_error.log
    <span class="token directive-inline property">CustomLog</span> <span class="token variable">${APACHE_LOG_DIR}</span>/email_autoconf_access.log combined
    <span class="token directive-inline property">ServerAdmin</span> hostmaster@example.org
    
    <span class="token directive-inline property">ProxyPass</span> / uwsgi://127.0.0.1:3032/
<span class="token directive-block tag"><span class="token directive-block tag"><span class="token punctuation">&#x3C;/</span>VirtualHost</span><span class="token punctuation">></span></span>

<span class="token directive-block tag"><span class="token directive-block tag"><span class="token punctuation">&#x3C;</span>VirtualHost</span><span class="token directive-block-parameter attr-value"> *<span class="token punctuation">:</span>443</span><span class="token punctuation">></span></span>
    <span class="token directive-inline property">ServerName</span> autoconfig.example.org
    <span class="token directive-inline property">ServerAlias</span> autoconfig.*
    <span class="token directive-inline property">ServerAlias</span> maildiscovery.example.org maildiscovery.*
    <span class="token directive-inline property">ServerAlias</span> autodiscover.example.org autodiscover.*
    <span class="token directive-inline property">SSLEngine</span> on
    <span class="token directive-inline property">SSLCertificateKeyFile</span> /etc/letsencrypt/live/maildiscovery.example.org/privkey.pem
    <span class="token directive-inline property">SSLCertificateFile</span> /etc/letsencrypt/live/maildiscovery.example.org/fullchain.pem
    <span class="token directive-inline property">ErrorLog</span> <span class="token variable">${APACHE_LOG_DIR}</span>/email_autoconf_error.log
    <span class="token directive-inline property">CustomLog</span> <span class="token variable">${APACHE_LOG_DIR}</span>/email_autoconf_access.log combined
    <span class="token directive-inline property">ServerAdmin</span> hostmaster@example.com
    
    <span class="token directive-inline property">ProxyPass</span> / uwsgi://127.0.0.1:3032/
<span class="token directive-block tag"><span class="token directive-block tag"><span class="token punctuation">&#x3C;/</span>VirtualHost</span><span class="token punctuation">></span></span>
</code></pre></div>
<h3 id="nginx"><a href="#nginx" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Nginx</h3>
<p>For Nginx a config like this should work:</p>
<div class="nuxt-content-highlight"><span class="filename">email-autoconf.conf</span><pre class="line-numbers language-nginx"><code><span class="token directive"><span class="token keyword">server</span></span> <span class="token punctuation">{</span>
    <span class="token directive"><span class="token keyword">listen</span> <span class="token number">80</span></span><span class="token punctuation">;</span>
    <span class="token directive"><span class="token keyword">server_name</span> autoconfig.example.org autoconfig.* maildiscovery.* autodiscovery.* autodiscover.*</span><span class="token punctuation">;</span>
    <span class="token directive"><span class="token keyword">location</span> /</span> <span class="token punctuation">{</span>
        <span class="token directive"><span class="token keyword">proxy_pass</span> http://127.0.0.1:3032/</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>

<span class="token directive"><span class="token keyword">server</span></span> <span class="token punctuation">{</span>
    <span class="token directive"><span class="token keyword">listen</span> <span class="token number">443</span> ssl</span><span class="token punctuation">;</span>
    <span class="token directive"><span class="token keyword">server_name</span> autoconfig.example.org autoconfig.* maildiscovery.* autodiscovery.* autodiscover.*</span><span class="token punctuation">;</span>
    <span class="token directive"><span class="token keyword">ssl_certificate_key</span> /etc/letsencrypt/live/maildiscovery.example.org/privkey.pem</span><span class="token punctuation">;</span>
    <span class="token directive"><span class="token keyword">ssl_certificate</span> /etc/letsencrypt/live/maildiscovery.example.org/fullchain.pem</span><span class="token punctuation">;</span>
    <span class="token directive"><span class="token keyword">ssl_protocols</span> TLSv1.2 TLSv1.3</span><span class="token punctuation">;</span>
    <span class="token directive"><span class="token keyword">location</span> /</span> <span class="token punctuation">{</span>
        <span class="token directive"><span class="token keyword">proxy_pass</span> http://127.0.0.1:3032/</span><span class="token punctuation">;</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Check if email-autoconf is running (presents a nice splash screen with Onlime logo): <a href="https://maildiscovery.example.org/" rel="nofollow noopener noreferrer" target="_blank">https://maildiscovery.example.org/</a></p>
<h2 id="testing"><a href="#testing" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Testing</h2>
<h3 id="email_autoconf_testpy"><a href="#email_autoconf_testpy" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>email_autoconf_test.py</h3>
<p>Use <code>scripts/email_autoconf_test.py</code> helper script to test any self-hosted or external email address. Make sure you execute that script under your <code>venv</code> which is correctly set up as described in above installation section:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token builtin class-name">cd</span> /var/www/email-autoconf
$ <span class="token builtin class-name">.</span> venv/bin/activate
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ <span class="token builtin class-name">cd</span> scripts
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ python3 email_autoconf_test.py demo@example.com
</code></pre></div>
<p>Sample result (full XML output for all 3 protocols Mozilla Autoconfig, Microsoft Autodiscover, Apple Mobileconfig):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-xml"><code>Testing Autoconfig
Try https://autoconfig.example.org/mail/config-v1.1.xml?emailaddress=demo@example.com
Autoconfig content:

<span class="token prolog">&#x3C;?xml version="1.0" encoding="UTF-8"?></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>clientConfig</span> <span class="token attr-name">version</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>1.1<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>emailProvider</span> <span class="token attr-name">id</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>example.org<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>domain</span><span class="token punctuation">></span></span>example.com<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>domain</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>displayName</span><span class="token punctuation">></span></span>demo@example.com<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>displayName</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>incomingServer</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>imap<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>hostname</span><span class="token punctuation">></span></span>imap.example.org<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>hostname</span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>port</span><span class="token punctuation">></span></span>993<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>port</span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>socketType</span><span class="token punctuation">></span></span>SSL<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>socketType</span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>authentication</span><span class="token punctuation">></span></span>password-cleartext<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>authentication</span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>username</span><span class="token punctuation">></span></span>demo_example_com<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>username</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>incomingServer</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>incomingServer</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>pop3<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>hostname</span><span class="token punctuation">></span></span>pop.example.org<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>hostname</span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>port</span><span class="token punctuation">></span></span>995<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>port</span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>socketType</span><span class="token punctuation">></span></span>SSL<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>socketType</span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>authentication</span><span class="token punctuation">></span></span>password-cleartext<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>authentication</span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>username</span><span class="token punctuation">></span></span>demo_example_com<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>username</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>incomingServer</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>outgoingServer</span> <span class="token attr-name">type</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>smtp<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>hostname</span><span class="token punctuation">></span></span>smtp.example.org<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>hostname</span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>port</span><span class="token punctuation">></span></span>465<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>port</span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>socketType</span><span class="token punctuation">></span></span>SSL<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>socketType</span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>username</span><span class="token punctuation">></span></span>demo_example_com<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>username</span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>authentication</span><span class="token punctuation">></span></span>password-cleartext<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>authentication</span><span class="token punctuation">></span></span>
         <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>useGlobalPreferredServer</span><span class="token punctuation">></span></span>no<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>useGlobalPreferredServer</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>outgoingServer</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>emailProvider</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>clientConfig</span><span class="token punctuation">></span></span>

---
Testing Autodiscover
Try https://maildiscovery.example.org/autodiscover/autodiscover.xml
Autodiscover content:

<span class="token prolog">&#x3C;?xml version="1.0" encoding="UTF-8"?></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Autodiscover</span> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://schemas.microsoft.com/exchange/autodiscover/responseschema/2006<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Response</span> <span class="token attr-name">xmlns</span><span class="token attr-value"><span class="token punctuation attr-equals">=</span><span class="token punctuation">"</span>http://schemas.microsoft.com/exchange/autodiscover/outlook/responseschema/2006a<span class="token punctuation">"</span></span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>User</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>DisplayName</span><span class="token punctuation">></span></span>demo@example.com<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>DisplayName</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>User</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Account</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>AccountType</span><span class="token punctuation">></span></span>email<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>AccountType</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Action</span><span class="token punctuation">></span></span>settings<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Action</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Protocol</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Type</span><span class="token punctuation">></span></span>IMAP<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Type</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Server</span><span class="token punctuation">></span></span>imap.example.org<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Server</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Port</span><span class="token punctuation">></span></span>993<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Port</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>SPA</span><span class="token punctuation">></span></span>off<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>SPA</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Encryption</span><span class="token punctuation">></span></span>Auto<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Encryption</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>SSL</span><span class="token punctuation">></span></span>on<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>SSL</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>AuthRequired</span><span class="token punctuation">></span></span>on<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>AuthRequired</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>DomainRequired</span><span class="token punctuation">></span></span>off<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>DomainRequired</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>LoginName</span><span class="token punctuation">></span></span>demo_example_com<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>LoginName</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Protocol</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Protocol</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Type</span><span class="token punctuation">></span></span>POP3<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Type</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Server</span><span class="token punctuation">></span></span>pop.example.org<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Server</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Port</span><span class="token punctuation">></span></span>995<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Port</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>SPA</span><span class="token punctuation">></span></span>off<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>SPA</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Encryption</span><span class="token punctuation">></span></span>Auto<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Encryption</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>SSL</span><span class="token punctuation">></span></span>on<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>SSL</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>AuthRequired</span><span class="token punctuation">></span></span>on<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>AuthRequired</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>DomainRequired</span><span class="token punctuation">></span></span>off<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>DomainRequired</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>LoginName</span><span class="token punctuation">></span></span>demo_example_com<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>LoginName</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Protocol</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Protocol</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Type</span><span class="token punctuation">></span></span>SMTP<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Type</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Server</span><span class="token punctuation">></span></span>smtp.example.org<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Server</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Port</span><span class="token punctuation">></span></span>587<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Port</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>SPA</span><span class="token punctuation">></span></span>off<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>SPA</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>Encryption</span><span class="token punctuation">></span></span>Auto<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Encryption</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>SSL</span><span class="token punctuation">></span></span>on<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>SSL</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>AuthRequired</span><span class="token punctuation">></span></span>on<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>AuthRequired</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>UsePOPAuth</span><span class="token punctuation">></span></span>on<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>UsePOPAuth</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>SMTPLast</span><span class="token punctuation">></span></span>off<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>SMTPLast</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>DomainRequired</span><span class="token punctuation">></span></span>off<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>DomainRequired</span><span class="token punctuation">></span></span>
        <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;</span>LoginName</span><span class="token punctuation">></span></span>demo_example_com<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>LoginName</span><span class="token punctuation">></span></span>
      <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Protocol</span><span class="token punctuation">></span></span>
    <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Account</span><span class="token punctuation">></span></span>
  <span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Response</span><span class="token punctuation">></span></span>
<span class="token tag"><span class="token tag"><span class="token punctuation">&#x3C;/</span>Autodiscover</span><span class="token punctuation">></span></span>

---
Testing Mobileconfig
Try https://autoconfig.example.org/email.mobileconfig?email=demo@example.com
(...)
</code></pre></div>
<p>Cool! Now, head over to your email client and do some live testing. If all works well, you should just be required to enter the email address and password, the rest should get auto detected. It works best on Thunderbird. Outlook may struggle with it, depending on your Outlook version.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="email" term="email"/>
        <category label="python" term="python"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[GitLab CI/CD of a Nuxt.js frontend over SSH/rsync]]></title>
        <id>https://pipo.blog/articles/20210808-gitlab-cd-rsync-deployment</id>
        <link href="https://pipo.blog/articles/20210808-gitlab-cd-rsync-deployment"/>
        <updated>2021-08-08T12:10:00.000Z</updated>
        <summary type="html"><![CDATA[Simple SSH/rsync based Continuous Deployment of a Nuxt.js static frontend project with GitLab CI/CD.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/gitlab-ci-cd-test-deploy.png"></p>
                      <p>Continuous deployment is great and a must for every modern web app. Forget about the times when you had to constantly log into your production server over SSH to run some <code>git pull</code> based deployment and cumbersome and error-prone build tasks, or building your project locally and then deploying it with <code>rsync</code> to production, which is not that sexy either. It is all doable and scriptable, but we want to have the whole process automated without any manual work involved. I much prefer to use <a href="https://docs.gitlab.com/ee/ci/" rel="nofollow noopener noreferrer" target="_blank">GitLab CI/CD</a> over <a href="https://github.com/features/actions" rel="nofollow noopener noreferrer" target="_blank">GitHub Actions</a> - ok, mainly just because I am more into it and prefer to run a self-hosted GitLab instance. GitLab just gets the job done very well!</p>
<p>We want the whole process to be straightforward without any fancy extras. It should be a matter of 15mins to set it up on every new project and I don't like to introduce any extra dependencies. I am just talking about deployment of a static site / SPA, a <a href="https://vuejs.org/" rel="nofollow noopener noreferrer" target="_blank">Vue.js</a> based frontend that is generated by <a href="https://nuxtjs.org/" rel="nofollow noopener noreferrer" target="_blank">Nuxt.js</a>. So let's keep it simple here! I am going to present you the solution I am using to deploy this TechBlog.</p>
<!--more-->
<h2 id="requirements"><a href="#requirements" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Requirements</h2>
<p>OK, don't do it in the most simple way, we still have some basic requirements aside an apple a day (which keeps the doctor away):</p>
<ul>
<li>Building should be done by <strong><a href="https://docs.gitlab.com/ee/ci/" rel="nofollow noopener noreferrer" target="_blank">GitLab CI/CD</a></strong> in <strong>Docker executor</strong></li>
<li>Generated static site is uploaded to production server via <strong>SSH/rsync</strong></li>
<li>We want <strong>basic versioning with <code>current</code> symlink</strong>, keeping the last 5 releases. No fat dependencies like <a href="https://capistranorb.com/" rel="nofollow noopener noreferrer" target="_blank">Capistrano</a>, please!</li>
<li><strong>Only deploy when pushed to the default (<code>main</code>) branch</strong> (or any other branch that fits your workflow, but don't deploy on every branch!)</li>
</ul>
<h2 id="manual-deployment"><a href="#manual-deployment" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Manual Deployment</h2>
<blockquote>
<p><strong>Remember:</strong> Building the application, generate every route as a HTML file and statically export to <code>dist/</code> directory on a <a href="https://nuxtjs.org/" rel="nofollow noopener noreferrer" target="_blank">Nuxt.js</a> project goes like this (I am preferring <code>yarn</code> over <code>npm</code>, but basically the same):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">yarn</span> <span class="token function">install</span>
$ <span class="token function">yarn</span> generate
</code></pre></div>
<p><code>yarn generate</code> maps to <code>nuxt generate</code>, as we have defined the following in <code>package.json</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-json"><code><span class="token punctuation">{</span>
     <span class="token property">"scripts"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
       <span class="token property">"dev"</span><span class="token operator">:</span> <span class="token string">"nuxt"</span><span class="token punctuation">,</span>
       <span class="token property">"build"</span><span class="token operator">:</span> <span class="token string">"nuxt build"</span><span class="token punctuation">,</span>
       <span class="token property">"start"</span><span class="token operator">:</span> <span class="token string">"nuxt start"</span><span class="token punctuation">,</span>
       <span class="token property">"generate"</span><span class="token operator">:</span> <span class="token string">"nuxt generate"</span>
     <span class="token punctuation">}</span><span class="token punctuation">,</span>
     <span class="token comment">// ...</span>
<span class="token punctuation">}</span>
</code></pre></div>
</blockquote>
<p>If a local build does not yet work for you (maybe producing some HTML/CSS minification errors...), fix it first locally and study <a href="https://nuxtjs.org/docs/2.x/get-started/commands" rel="nofollow noopener noreferrer" target="_blank">Nuxt.js Commands and Deployment</a>. In this example and on this TechBlog deployment, we are using <code>target: 'static'</code> in <code>nuxt.config.js</code>, that's why we build the site with <code>nuxt generate</code> instead of <code>nuxt build</code>.</p>
<p>On a manual deployment, you would then rsync the whole <code>dist/</code> content to your production server, e.g.:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">rsync</span> <span class="token parameter variable">-aHv</span> <span class="token parameter variable">--delete</span> dist/ user@example.com:public_html/blog
</code></pre></div>
<p>On production server, you would then point the site root to <code>public_html/blog</code>, but with the automated CD solution below, you would point it to <code>public_html/blog/current</code>, as we're going to create a symlink to the current release.</p>
<h2 id="gitlab-cicd-setup"><a href="#gitlab-cicd-setup" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>GitLab CI/CD Setup</h2>
<ul>
<li><a href="https://docs.gitlab.com/ee/ci/ssh_keys/#ssh-keys-when-using-the-docker-executor" rel="nofollow noopener noreferrer" target="_blank">GitLab CI/CD: SSH keys when using the Docker executor</a></li>
<li><a href="https://docs.gitlab.com/ee/ssh/index.html#generate-an-ssh-key-pair" rel="nofollow noopener noreferrer" target="_blank">GitLab: Generate an SSH key pair</a></li>
<li><a href="https://docs.gitlab.com/ee/ci/ssh_keys/#verifying-the-ssh-host-keys" rel="nofollow noopener noreferrer" target="_blank">GitLab: Verifying the SSH host keys</a></li>
</ul>
<h3 id="set-up-cicd-variables"><a href="#set-up-cicd-variables" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Set up CI/CD Variables</h3>
<p>Building and deployment is done automatically via GitLab CI/CD, once you have <code>.gitlab-ci.yml</code> (see below) added to your project root. A deployment is always triggered when you push to <code>main</code> branch. But first, set up those CI/CD variables in your GitLab project under <strong>Settings > CI/CD > Variables</strong>:</p>
<ul>
<li><code>DEPLOY_HOST</code> (Variable): The production host we want to deploy to.</li>
<li><code>SSH_KNOWN_HOSTS</code> (Variable): <code>~/.ssh/known_hosts</code> lines on deployment container (Docker)</li>
<li><code>SSH_USER</code> (Variable): The user to connect to the production host.</li>
<li><code>SSH_KEY</code> (<strong>File</strong>): The deployment SSH private key (copy-paste the whole key starting with <code>-----BEGIN OPENSSH PRIVATE KEY-----\n...</code>).</li>
</ul>
<p>To get the correct line(s) for <code>SSH_KNOWN_HOSTS</code> variable, run this from a trusted host:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ ssh-keyscan DEPLOY_HOST
</code></pre></div>
<p>Initially, you create a SSH keypair as follows (ED25519 is the first-preference signature algorithm since <a href="https://www.openssh.com/txt/release-8.5" rel="nofollow noopener noreferrer" target="_blank">OpenSSH 8.5</a> and has been supported since OpenSSH 6.5):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ ssh-keygen <span class="token parameter variable">-t</span> ed25519 <span class="token parameter variable">-f</span> ~/.ssh/deploy_id_ed25519 <span class="token parameter variable">-N</span> <span class="token string">""</span> <span class="token parameter variable">-C</span> <span class="token string">"gitlab-ci-deployer"</span>
$ <span class="token function">cat</span> ~/.ssh/deploy_id_ed25519.pub
</code></pre></div>
<p>Don't store the private key anywhere else than in <code>$SSH_KEY</code>! Copy the public key (<code>deploy_id_ed25519.pub</code>) to the deploy host's <code>authorized_keys</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token builtin class-name">echo</span> <span class="token string">"ssh-ed25519 AAAAC3N... gitlab-ci-deployer"</span> <span class="token operator">>></span> ~/.ssh/authorized_keys
</code></pre></div>
<h3 id="create-gitlab-ciyml"><a href="#create-gitlab-ciyml" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Create <code>.gitlab-ci.yml</code></h3>
<p>Ready for the real show? We're using official <a href="https://hub.docker.com/_/node" rel="nofollow noopener noreferrer" target="_blank">node Docker Image</a>. <code>node:current</code> is based on Debian Buster (as of Aug 2021), so we can also use <code>apt-get</code> inside the container to install our extras, which is actually just <code>rsync</code>, nothing more.</p>
<p>Create the following <code>.gitlab-ci.yml</code> in your project root. It will use the private SSH key which you have provided in <code>SSH_KEY</code> GitLab CI/CD Variables as a <code>File</code> (NOT as a <code>Variable</code>!), so you don't need to know about the actual location of the key, just use <code>ssh -i ${SSH_KEY}</code> in the deploy commands.</p>
<div class="nuxt-content-highlight"><span class="filename">.gitlab-ci.yml</span><pre class="line-numbers language-yaml"><code><span class="token key atrule">image</span><span class="token punctuation">:</span> node<span class="token punctuation">:</span>current

<span class="token key atrule">variables</span><span class="token punctuation">:</span>
  <span class="token comment"># In case you did not set up $SSH_KNOWN_HOSTS variable:</span>
  <span class="token comment"># SSH_OPTS: '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'</span>
  <span class="token key atrule">SSH_OPTS</span><span class="token punctuation">:</span> <span class="token string">''</span>
  <span class="token key atrule">BASE_DIR</span><span class="token punctuation">:</span> public_html/blog
  <span class="token key atrule">KEEP</span><span class="token punctuation">:</span> <span class="token number">5</span>

<span class="token key atrule">default</span><span class="token punctuation">:</span>
  <span class="token key atrule">before_script</span><span class="token punctuation">:</span>
    <span class="token punctuation">-</span> mkdir <span class="token punctuation">-</span>p ~/.ssh <span class="token important">&#x26;&#x26;</span> chmod 700 ~/.ssh
    <span class="token punctuation">-</span> echo "$SSH_KNOWN_HOSTS" <span class="token punctuation">></span> ~/.ssh/known_hosts
    <span class="token punctuation">-</span> chmod 644 ~/.ssh/known_hosts
    <span class="token punctuation">-</span> chmod 600 $<span class="token punctuation">{</span>SSH_KEY<span class="token punctuation">}</span>
    <span class="token punctuation">-</span> export COMMIT_TIME=$(git show <span class="token punctuation">-</span>s <span class="token punctuation">-</span><span class="token punctuation">-</span>format=%ct $CI_COMMIT_SHA)

<span class="token key atrule">cache</span><span class="token punctuation">:</span>
  <span class="token key atrule">paths</span><span class="token punctuation">:</span>
    <span class="token punctuation">-</span> node_modules

<span class="token key atrule">build</span><span class="token punctuation">:</span>
  <span class="token key atrule">stage</span><span class="token punctuation">:</span> build
  <span class="token key atrule">script</span><span class="token punctuation">:</span>
    <span class="token punctuation">-</span> cp .env.prod .env
    <span class="token punctuation">-</span> yarn install
    <span class="token punctuation">-</span> yarn generate
  <span class="token key atrule">artifacts</span><span class="token punctuation">:</span>
    <span class="token key atrule">paths</span><span class="token punctuation">:</span>
      <span class="token punctuation">-</span> dist
  <span class="token key atrule">rules</span><span class="token punctuation">:</span>
    <span class="token punctuation">-</span> <span class="token key atrule">if</span><span class="token punctuation">:</span> $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    <span class="token punctuation">-</span> <span class="token key atrule">if</span><span class="token punctuation">:</span> $RUN_ALWAYS

<span class="token key atrule">deploy</span><span class="token punctuation">:</span>
  <span class="token key atrule">stage</span><span class="token punctuation">:</span> deploy
  <span class="token key atrule">script</span><span class="token punctuation">:</span>
    <span class="token punctuation">-</span> apt<span class="token punctuation">-</span>get update <span class="token punctuation">-</span>y <span class="token important">&#x26;&#x26;</span> apt<span class="token punctuation">-</span>get install <span class="token punctuation">-</span>y rsync
    <span class="token punctuation">-</span> ssh <span class="token punctuation">-</span>i $<span class="token punctuation">{</span>SSH_KEY<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_OPTS<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_USER<span class="token punctuation">}</span>@$<span class="token punctuation">{</span>DEPLOY_HOST<span class="token punctuation">}</span> "cd $<span class="token punctuation">{</span>BASE_DIR<span class="token punctuation">}</span> <span class="token important">&#x26;&#x26;</span> test <span class="token tag">!</span> <span class="token punctuation">-</span>h current <span class="token punctuation">|</span><span class="token punctuation">|</span> cp <span class="token punctuation">-</span>r \$(ls <span class="token punctuation">-</span>td <span class="token important">*/</span> <span class="token punctuation">|</span> grep <span class="token punctuation">-</span>v current <span class="token punctuation">|</span> head <span class="token punctuation">-</span>n1) deploy_tmp"
    <span class="token punctuation">-</span> rsync <span class="token punctuation">-</span>aHv <span class="token punctuation">-</span><span class="token punctuation">-</span>delete <span class="token punctuation">-</span>e "ssh <span class="token punctuation">-</span>i $<span class="token punctuation">{</span>SSH_KEY<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_OPTS<span class="token punctuation">}</span>" dist/ $<span class="token punctuation">{</span>SSH_USER<span class="token punctuation">}</span>@$<span class="token punctuation">{</span>DEPLOY_HOST<span class="token punctuation">}</span><span class="token punctuation">:</span>$<span class="token punctuation">{</span>BASE_DIR<span class="token punctuation">}</span>/deploy_tmp
    <span class="token punctuation">-</span> ssh <span class="token punctuation">-</span>i $<span class="token punctuation">{</span>SSH_KEY<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_OPTS<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_USER<span class="token punctuation">}</span>@$<span class="token punctuation">{</span>DEPLOY_HOST<span class="token punctuation">}</span> "cd $<span class="token punctuation">{</span>BASE_DIR<span class="token punctuation">}</span> <span class="token important">&#x26;&#x26;</span> mv deploy_tmp $<span class="token punctuation">{</span>COMMIT_TIME<span class="token punctuation">}</span> <span class="token important">&#x26;&#x26;</span> rm <span class="token punctuation">-</span>f current <span class="token important">&#x26;&#x26;</span> ln <span class="token punctuation">-</span>s $<span class="token punctuation">{</span>COMMIT_TIME<span class="token punctuation">}</span> current"
    <span class="token punctuation">-</span> ssh <span class="token punctuation">-</span>i $<span class="token punctuation">{</span>SSH_KEY<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_OPTS<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_USER<span class="token punctuation">}</span>@$<span class="token punctuation">{</span>DEPLOY_HOST<span class="token punctuation">}</span> "cd $<span class="token punctuation">{</span>BASE_DIR<span class="token punctuation">}</span> <span class="token important">&#x26;&#x26;</span> rm <span class="token punctuation">-</span>rf \$(ls <span class="token punctuation">-</span>dt <span class="token important">*/</span> <span class="token punctuation">|</span> grep <span class="token punctuation">-</span>v current <span class="token punctuation">|</span> tail <span class="token punctuation">-</span>n +$(($KEEP+1)))"
  <span class="token key atrule">rules</span><span class="token punctuation">:</span>
    <span class="token punctuation">-</span> <span class="token key atrule">if</span><span class="token punctuation">:</span> $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    <span class="token punctuation">-</span> <span class="token key atrule">if</span><span class="token punctuation">:</span> $RUN_ALWAYS
</code></pre></div>
<blockquote>
<p><strong>UPDATE 2021-08-18:</strong> Above <code>.gitlab-ci.yml</code> can be further optimized, so that we no longer need to provide <code>$RUN_ALWAYS</code> variable to force-trigger a pipeline run on a different branch than default (<code>main</code> / <code>master</code>). I have also combined <code>build</code> and <code>deploy</code> stages into a single one, for improved deployment speed, as we don't need to split this up for this simple project. Like this, <code>dist</code> artifacts are no longer needed:</p>
</blockquote>
<div class="nuxt-content-highlight"><span class="filename">.gitlab-ci.yml</span><pre class="line-numbers language-yaml"><code><span class="token key atrule">image</span><span class="token punctuation">:</span> node<span class="token punctuation">:</span>current

<span class="token key atrule">variables</span><span class="token punctuation">:</span>
  <span class="token comment"># In case you did not set up $SSH_KNOWN_HOSTS variable:</span>
  <span class="token comment"># SSH_OPTS: '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null'</span>
  <span class="token key atrule">SSH_OPTS</span><span class="token punctuation">:</span> <span class="token string">''</span>
  <span class="token key atrule">BASE_DIR</span><span class="token punctuation">:</span> public_html/blog
  <span class="token key atrule">KEEP</span><span class="token punctuation">:</span> <span class="token number">5</span>

<span class="token key atrule">default</span><span class="token punctuation">:</span>
  <span class="token key atrule">before_script</span><span class="token punctuation">:</span>
    <span class="token punctuation">-</span> mkdir <span class="token punctuation">-</span>p ~/.ssh <span class="token important">&#x26;&#x26;</span> chmod 700 ~/.ssh
    <span class="token punctuation">-</span> echo "$SSH_KNOWN_HOSTS" <span class="token punctuation">></span> ~/.ssh/known_hosts
    <span class="token punctuation">-</span> chmod 644 ~/.ssh/known_hosts
    <span class="token punctuation">-</span> chmod 600 $<span class="token punctuation">{</span>SSH_KEY<span class="token punctuation">}</span>
    <span class="token punctuation">-</span> export COMMIT_TIME=$(git show <span class="token punctuation">-</span>s <span class="token punctuation">-</span><span class="token punctuation">-</span>format=%ct $CI_COMMIT_SHA)

<span class="token key atrule">cache</span><span class="token punctuation">:</span>
  <span class="token key atrule">paths</span><span class="token punctuation">:</span>
    <span class="token punctuation">-</span> node_modules

<span class="token comment"># combined build and deploy step to speed it up</span>
<span class="token key atrule">build_deploy</span><span class="token punctuation">:</span>
  <span class="token key atrule">stage</span><span class="token punctuation">:</span> deploy
  <span class="token key atrule">script</span><span class="token punctuation">:</span>
    <span class="token comment"># build</span>
    <span class="token punctuation">-</span> cp .env.prod .env
    <span class="token punctuation">-</span> yarn install
    <span class="token punctuation">-</span> yarn generate
    <span class="token comment"># deploy</span>
    <span class="token punctuation">-</span> apt<span class="token punctuation">-</span>get update <span class="token punctuation">-</span>y <span class="token important">&#x26;&#x26;</span> apt<span class="token punctuation">-</span>get install <span class="token punctuation">-</span>y rsync
    <span class="token punctuation">-</span> ssh <span class="token punctuation">-</span>i $<span class="token punctuation">{</span>SSH_KEY<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_OPTS<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_USER<span class="token punctuation">}</span>@$<span class="token punctuation">{</span>DEPLOY_HOST<span class="token punctuation">}</span> "cd $<span class="token punctuation">{</span>BASE_DIR<span class="token punctuation">}</span> <span class="token important">&#x26;&#x26;</span> test <span class="token tag">!</span> <span class="token punctuation">-</span>h current <span class="token punctuation">|</span><span class="token punctuation">|</span> cp <span class="token punctuation">-</span>r \$(ls <span class="token punctuation">-</span>td <span class="token important">*/</span> <span class="token punctuation">|</span> grep <span class="token punctuation">-</span>v current <span class="token punctuation">|</span> head <span class="token punctuation">-</span>n1) deploy_tmp"
    <span class="token punctuation">-</span> rsync <span class="token punctuation">-</span>aHv <span class="token punctuation">-</span><span class="token punctuation">-</span>delete <span class="token punctuation">-</span>e "ssh <span class="token punctuation">-</span>i $<span class="token punctuation">{</span>SSH_KEY<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_OPTS<span class="token punctuation">}</span>" dist/ $<span class="token punctuation">{</span>SSH_USER<span class="token punctuation">}</span>@$<span class="token punctuation">{</span>DEPLOY_HOST<span class="token punctuation">}</span><span class="token punctuation">:</span>$<span class="token punctuation">{</span>BASE_DIR<span class="token punctuation">}</span>/deploy_tmp
    <span class="token punctuation">-</span> ssh <span class="token punctuation">-</span>i $<span class="token punctuation">{</span>SSH_KEY<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_OPTS<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_USER<span class="token punctuation">}</span>@$<span class="token punctuation">{</span>DEPLOY_HOST<span class="token punctuation">}</span> "cd $<span class="token punctuation">{</span>BASE_DIR<span class="token punctuation">}</span> <span class="token important">&#x26;&#x26;</span> mv deploy_tmp $<span class="token punctuation">{</span>COMMIT_TIME<span class="token punctuation">}</span> <span class="token important">&#x26;&#x26;</span> rm <span class="token punctuation">-</span>f current <span class="token important">&#x26;&#x26;</span> ln <span class="token punctuation">-</span>s $<span class="token punctuation">{</span>COMMIT_TIME<span class="token punctuation">}</span> current"
    <span class="token punctuation">-</span> ssh <span class="token punctuation">-</span>i $<span class="token punctuation">{</span>SSH_KEY<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_OPTS<span class="token punctuation">}</span> $<span class="token punctuation">{</span>SSH_USER<span class="token punctuation">}</span>@$<span class="token punctuation">{</span>DEPLOY_HOST<span class="token punctuation">}</span> "cd $<span class="token punctuation">{</span>BASE_DIR<span class="token punctuation">}</span> <span class="token important">&#x26;&#x26;</span> rm <span class="token punctuation">-</span>rf \$(ls <span class="token punctuation">-</span>dt <span class="token important">*/</span> <span class="token punctuation">|</span> grep <span class="token punctuation">-</span>v current <span class="token punctuation">|</span> tail <span class="token punctuation">-</span>n +$(($KEEP+1)))"
  <span class="token key atrule">rules</span><span class="token punctuation">:</span>
    <span class="token punctuation">-</span> <span class="token key atrule">if</span><span class="token punctuation">:</span> $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
    <span class="token punctuation">-</span> <span class="token key atrule">if</span><span class="token punctuation">:</span> $CI_PIPELINE_SOURCE == "web"
</code></pre></div>
<p>From now on, once you push to the default (usually  <code>main</code> or <code>master</code>) branch, continuous deployment is triggered.</p>
<blockquote>
<p>If you explicitely want to skip CI/CD for a single push, use:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">git</span> push <span class="token parameter variable">-o</span> ci.skip
</code></pre></div>
<p>GitLab will respect it!</p>
</blockquote>
<p>If you wish to run a manual pipeline on a different branch, just do this from Gitlab's <strong>Run pipeline</strong> form. The <code>$CI_PIPELINE_SOURCE == "web"</code> rule comes into play!</p>
<p>The deployment task cares about basic versioning, so the final result will be such a structure (the Unix timestamp was taken from the latest git commit), while the oldest releases are getting rotated away (see <code>KEEP</code> variable in <code>.gitlab-ci.yml</code>):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>public_html/blog
├── 1629150195
├── 1629154265
├── 1629154699
├── 1629188761
├── 1629188770
└── current -> 1629188770
</code></pre></div>
<p>Honestly, the deploy commands don't look that sexy in our <code>.gitlab-ci.yml</code>. But we are perfectly happy with this, especially since we didn't introduce any new monster dependency like <a href="https://capistranorb.com/" rel="nofollow noopener noreferrer" target="_blank">Capistrano</a> or similar deployment tools.</p>
<p>Finally, point your production web server's virtual host to <code>public_html/blog/current</code>. No need to restart anything or flush any caches, as this is a static site / SPA only.</p>
<p>Done! Now, go back coding and no longer care about deployment.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="devops" term="devops"/>
        <category label="gitlab" term="gitlab"/>
        <category label="linux" term="linux"/>
        <category label="vue.js" term="vue.js"/>
        <category label="nuxt.js" term="nuxt.js"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Cleanup helper script to destroy old ZFS snapshots]]></title>
        <id>https://pipo.blog/articles/20210806-zfs-destroy-snapshots</id>
        <link href="https://pipo.blog/articles/20210806-zfs-destroy-snapshots"/>
        <updated>2021-08-06T13:00:00.000Z</updated>
        <summary type="html"><![CDATA[A small helper script written in Python to destroy legacy ZFS snapshots.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/open-zfs-blueprint.jpg"></p>
                      <p>For years now, I am using this little helper script written in Python, which solves a very basic task: Destroy legacy ZFS snapshots. We all know ZFS snapshots are so lightweight and we tend to create so many snapshots, either manually (e.g. before a major system upgrade) or automated (e.g. by our replication or backup jobs). It can somtimes be cumbersome to destroy legacy ZFS snapshots that are no longer needed.</p>
<p>So, here comes help with <code>zfs-destroy-snapshots.py</code> which you could deploy to <code>/usr/local/sbin</code> to have that handy script ready. I know, this is like a really small snipped I could just post to Github Gist (ok, here you go: <a href="https://gist.github.com/onlime/530c27b71056efd6edd7a38b7369457d" rel="nofollow noopener noreferrer" target="_blank">onlime/zfs-destroy-snapshots.py</a>) or Pastebin, but I think it still deserves a full blown blog post.</p>
<!--more-->
<p>Below script supports the following options:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ zfs-destroy-snapshots.py <span class="token parameter variable">-h</span>
usage: zfs-destroy-snapshots.py <span class="token punctuation">[</span>-h<span class="token punctuation">]</span> <span class="token punctuation">[</span>--prefix PREFIX<span class="token punctuation">]</span> <span class="token punctuation">[</span>--exclude EXCLUDE<span class="token punctuation">]</span>
                                <span class="token punctuation">[</span>--dataset DATASET<span class="token punctuation">]</span> <span class="token punctuation">[</span>--destroy<span class="token punctuation">]</span>
                                age

positional arguments:
  age                List snapshots that are older than age <span class="token punctuation">(</span>hours<span class="token punctuation">)</span>.

optional arguments:
  -h, <span class="token parameter variable">--help</span>         show this <span class="token builtin class-name">help</span> message and <span class="token builtin class-name">exit</span>
  <span class="token parameter variable">--prefix</span> PREFIX    Only list snapshot with this prefix.
  <span class="token parameter variable">--exclude</span> EXCLUDE  Exclude snapshots that contain this string.
  <span class="token parameter variable">--dataset</span> DATASET  Limit to a specific dataset.
  <span class="token parameter variable">--destroy</span>          Destroy snapshots that are older than age.
</code></pre></div>
<p>So, basic use would be (run this as <code>root</code>):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token comment"># list all snapshots that are older than 48hrs</span>
$ zfs-destroy-snapshots.py <span class="token number">48</span>
<span class="token comment"># destroy them for good</span>
$ zfs-destroy-snapshots.py <span class="token number">48</span> <span class="token parameter variable">--destroy</span>
</code></pre></div>
<p>By default, the script does a dry-run, only listing snapshots it is going to destroy. It will only actually destroy them, if you explicitely use the <code>--destroy</code> argument. For your safety, and your daughter's.</p>
<p>You could further limit the lookup of snapshots by the following:</p>
<ul>
<li><code>--prefix</code>: snapshot prefix like e.g. <code>zrep_</code> for <a href="https://github.com/bolthole/zrep" rel="nofollow noopener noreferrer" target="_blank">zrep</a> or <code>rep_</code> for <a href="https://pve.proxmox.com/wiki/PVE-zsync" rel="nofollow noopener noreferrer" target="_blank">pve-zsync (Proxmox VE replication manager / backup tool)</a></li>
<li><code>--exclude</code>: exclude snapshots that contain this string</li>
<li><code>--dataset</code>: limit by a single dataset, e.g. <code>rpool/zfsdisks/subvol-123-disk-1</code></li>
</ul>
<p>Remember to always run this script without <code>--destroy</code> first to verify the snapshots which are going to be deleted.</p>
<p>Here you go, <code>zfs-destroy-snapshots.py</code> / Gist <a href="https://gist.github.com/onlime/530c27b71056efd6edd7a38b7369457d" rel="nofollow noopener noreferrer" target="_blank">onlime/zfs-destroy-snapshots.py</a></p>
<div class="nuxt-content-highlight"><span class="filename">zfs-destroy-snapshots.py</span><pre class="line-numbers language-python"><code><span class="token comment">#!/usr/bin/env python3</span>
<span class="token triple-quoted-string string">"""
Helper script to remove old ZFS snapshots
Copyright (c) Onlime GmbH, https://www.onlime.ch
"""</span>
<span class="token keyword">import</span> argparse
<span class="token keyword">from</span> re <span class="token keyword">import</span> <span class="token builtin">compile</span>
<span class="token keyword">from</span> datetime <span class="token keyword">import</span> datetime<span class="token punctuation">,</span> timedelta
<span class="token keyword">from</span> subprocess <span class="token keyword">import</span> check_output
<span class="token keyword">import</span> sys

<span class="token keyword">def</span> <span class="token function">list_snapshots</span><span class="token punctuation">(</span>age<span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">,</span> prefix<span class="token punctuation">:</span> <span class="token builtin">str</span> <span class="token operator">=</span> <span class="token boolean">None</span><span class="token punctuation">,</span> exclude<span class="token punctuation">:</span> <span class="token builtin">str</span> <span class="token operator">=</span> <span class="token boolean">None</span><span class="token punctuation">,</span> dataset<span class="token punctuation">:</span> <span class="token builtin">str</span> <span class="token operator">=</span> <span class="token boolean">None</span><span class="token punctuation">,</span> destroy<span class="token punctuation">:</span> <span class="token builtin">bool</span> <span class="token operator">=</span> <span class="token boolean">False</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token comment"># List all snapshot names of a dataset sorted by creation date; sample command:</span>
    <span class="token comment"># $ zfs list -H -t snapshot -o name,creation -s creation [&#x3C;dataset>]</span>
    cmd <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string">'zfs'</span><span class="token punctuation">,</span> <span class="token string">'list'</span><span class="token punctuation">,</span> <span class="token string">'-H'</span><span class="token punctuation">,</span> <span class="token string">'-t'</span><span class="token punctuation">,</span> <span class="token string">'snapshot'</span><span class="token punctuation">,</span> <span class="token string">'-o'</span><span class="token punctuation">,</span> <span class="token string">'name,creation'</span><span class="token punctuation">,</span> <span class="token string">'-s'</span><span class="token punctuation">,</span> <span class="token string">'creation'</span><span class="token punctuation">]</span>
    <span class="token keyword">if</span> dataset<span class="token punctuation">:</span>
        cmd<span class="token punctuation">.</span>append<span class="token punctuation">(</span>dataset<span class="token punctuation">)</span>
    snap_list <span class="token operator">=</span> check_output<span class="token punctuation">(</span>cmd<span class="token punctuation">)</span><span class="token punctuation">.</span>decode<span class="token punctuation">(</span>sys<span class="token punctuation">.</span>stdout<span class="token punctuation">.</span>encoding<span class="token punctuation">)</span>
    snap_lines <span class="token operator">=</span> snap_list<span class="token punctuation">.</span>splitlines<span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token comment"># reverse sort order (`zfs list` only supports asc sort order), newest snapshots on top</span>
    snap_lines<span class="token punctuation">.</span>reverse<span class="token punctuation">(</span><span class="token punctuation">)</span>

    <span class="token keyword">for</span> line <span class="token keyword">in</span> snap_lines<span class="token punctuation">:</span>
        snapshot<span class="token punctuation">,</span> creation <span class="token operator">=</span> line<span class="token punctuation">.</span>split<span class="token punctuation">(</span><span class="token string">'\t'</span><span class="token punctuation">)</span>
        <span class="token keyword">if</span> prefix <span class="token keyword">and</span> <span class="token string">'@{}'</span><span class="token punctuation">.</span><span class="token builtin">format</span><span class="token punctuation">(</span>prefix<span class="token punctuation">)</span> <span class="token keyword">not</span> <span class="token keyword">in</span> snapshot<span class="token punctuation">:</span>
            <span class="token keyword">continue</span>
        <span class="token keyword">if</span> exclude <span class="token keyword">and</span> exclude <span class="token keyword">in</span> snapshot<span class="token punctuation">:</span>
            <span class="token keyword">continue</span>
        snap_date <span class="token operator">=</span> datetime<span class="token punctuation">.</span>strptime<span class="token punctuation">(</span>creation<span class="token punctuation">,</span> <span class="token string">'%a %b %d %H:%M %Y'</span><span class="token punctuation">)</span>
        <span class="token keyword">if</span> snap_date <span class="token operator">&#x3C;</span> datetime<span class="token punctuation">.</span>now<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token operator">-</span>timedelta<span class="token punctuation">(</span>hours<span class="token operator">=</span>age<span class="token punctuation">)</span><span class="token punctuation">:</span>
            <span class="token keyword">print</span><span class="token punctuation">(</span><span class="token string">'{}{} {}'</span><span class="token punctuation">.</span><span class="token builtin">format</span><span class="token punctuation">(</span>
                <span class="token string">'DESTROYING '</span> <span class="token keyword">if</span> destroy <span class="token keyword">else</span> <span class="token string">''</span><span class="token punctuation">,</span>
                snap_date<span class="token punctuation">.</span>strftime<span class="token punctuation">(</span><span class="token string">'%Y-%m-%d %H:%M'</span><span class="token punctuation">)</span><span class="token punctuation">,</span> 
                snapshot
            <span class="token punctuation">)</span><span class="token punctuation">)</span>
            <span class="token keyword">if</span> destroy<span class="token punctuation">:</span>
                check_output<span class="token punctuation">(</span><span class="token punctuation">[</span><span class="token string">'zfs'</span><span class="token punctuation">,</span> <span class="token string">'destroy'</span><span class="token punctuation">,</span> snapshot<span class="token punctuation">]</span><span class="token punctuation">)</span>

<span class="token keyword">if</span> __name__ <span class="token operator">==</span> <span class="token string">"__main__"</span><span class="token punctuation">:</span> <span class="token comment"># Only use the argument parsing if the script was called as script and not imported</span>
    <span class="token comment"># argument parsing</span>
    parser <span class="token operator">=</span> argparse<span class="token punctuation">.</span>ArgumentParser<span class="token punctuation">(</span><span class="token punctuation">)</span>
    parser<span class="token punctuation">.</span>add_argument<span class="token punctuation">(</span><span class="token string">"age"</span><span class="token punctuation">,</span> <span class="token builtin">type</span><span class="token operator">=</span><span class="token builtin">int</span><span class="token punctuation">,</span> <span class="token builtin">help</span><span class="token operator">=</span><span class="token string">"List snapshots that are older than age (hours)."</span><span class="token punctuation">)</span>
    parser<span class="token punctuation">.</span>add_argument<span class="token punctuation">(</span><span class="token string">"--prefix"</span><span class="token punctuation">,</span> <span class="token builtin">help</span><span class="token operator">=</span><span class="token string">"Only list snapshot with this prefix."</span><span class="token punctuation">)</span>
    parser<span class="token punctuation">.</span>add_argument<span class="token punctuation">(</span><span class="token string">"--exclude"</span><span class="token punctuation">,</span> <span class="token builtin">help</span><span class="token operator">=</span><span class="token string">"Exclude snapshots that contain this string."</span><span class="token punctuation">)</span>
    parser<span class="token punctuation">.</span>add_argument<span class="token punctuation">(</span><span class="token string">"--dataset"</span><span class="token punctuation">,</span> <span class="token builtin">help</span><span class="token operator">=</span><span class="token string">"Limit to a specific dataset."</span><span class="token punctuation">)</span>
    parser<span class="token punctuation">.</span>add_argument<span class="token punctuation">(</span><span class="token string">"--destroy"</span><span class="token punctuation">,</span> action<span class="token operator">=</span><span class="token string">'store_true'</span><span class="token punctuation">,</span> <span class="token builtin">help</span><span class="token operator">=</span><span class="token string">"Destroy snapshots that are older than age."</span><span class="token punctuation">)</span>
    args <span class="token operator">=</span> parser<span class="token punctuation">.</span>parse_args<span class="token punctuation">(</span><span class="token punctuation">)</span>

    list_snapshots<span class="token punctuation">(</span>args<span class="token punctuation">.</span>age<span class="token punctuation">,</span> args<span class="token punctuation">.</span>prefix<span class="token punctuation">,</span> args<span class="token punctuation">.</span>exclude<span class="token punctuation">,</span> args<span class="token punctuation">.</span>dataset<span class="token punctuation">,</span> args<span class="token punctuation">.</span>destroy<span class="token punctuation">)</span>
</code></pre></div>
<p>some final thoughts...</p>
<p>Besides, <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime GmbH</a> webhosting services run 100% on <a href="https://www.debian.org/" rel="nofollow noopener noreferrer" target="_blank">Debian Linux</a>, mostly in LXC containers on <a href="https://proxmox.com/en/proxmox-ve" rel="nofollow noopener noreferrer" target="_blank">Proxmox VE</a> (PVE). If you don't know PVE yet - I can just strongly recommend it. They offer stunning support for <a href="https://openzfs.org" rel="nofollow noopener noreferrer" target="_blank">OpenZFS</a> - formerly known as ZFS-on-Linux, which lately runs super robust (which was definitely not the case 4-5 yrs ago!). So really, if you're not into ZFS yet, look into it. It is so much more than just another file system. Enough promo for today. Cheers!</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="zfs" term="zfs"/>
        <category label="python" term="python"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Configure ESLint and Prettier for Vue/Nuxt.js project in VS Code]]></title>
        <id>https://pipo.blog/articles/20210805-eslint-prettier-for-vscode</id>
        <link href="https://pipo.blog/articles/20210805-eslint-prettier-for-vscode"/>
        <updated>2021-08-05T17:48:00.000Z</updated>
        <summary type="html"><![CDATA[Quick and dirty VS Code setup of ESLint and Prettier for your next Vue/Nuxt.js project.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/prettier.jpg"></p>
                      <p>I'd like to share my recommended <a href="https://eslint.org/" rel="nofollow noopener noreferrer" target="_blank">ESLint</a> and <a href="https://prettier.io/" rel="nofollow noopener noreferrer" target="_blank">Prettier</a> configuration for a <a href="https://vuejs.org/" rel="nofollow noopener noreferrer" target="_blank">Vue.js</a> project built with <a href="https://nuxtjs.org/" rel="nofollow noopener noreferrer" target="_blank">Nuxt.js</a> (loving it!). As preferred IDE, I am using - guess what? - <a href="https://code.visualstudio.com/" rel="nofollow noopener noreferrer" target="_blank">VS Code</a>. This article is just considered as simple dev notes without digging any deeper, and it's based on my current "best practices".</p>
<p>Install <a href="https://eslint.org/" rel="nofollow noopener noreferrer" target="_blank">ESLint</a> and <a href="https://prettier.io/" rel="nofollow noopener noreferrer" target="_blank">Prettier</a> with all recommended plugins into <code>devDependencies</code> using <code>yarn</code> (you might as well use <code>npm</code>):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">yarn</span> <span class="token function">add</span> <span class="token parameter variable">--dev</span> eslint eslint-plugin-nuxt eslint-plugin-vue
$ <span class="token function">yarn</span> <span class="token function">add</span> <span class="token parameter variable">--dev</span> eslint-config-prettier prettier eslint-plugin-prettier
$ <span class="token function">yarn</span> <span class="token function">add</span> <span class="token parameter variable">--dev</span> @nuxtjs/eslint-module @nuxtjs/eslint-config @babel/eslint-parser
</code></pre></div>
<!--more-->
<p>The following lines were added to my <code>package.json</code> (only an extract, showing you the current versions as of Aug 2021):</p>
<div class="nuxt-content-highlight"><span class="filename">package.json</span><pre class="line-numbers language-json"><code><span class="token punctuation">{</span>
  <span class="token property">"devDependencies"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
    <span class="token property">"@babel/eslint-parser"</span><span class="token operator">:</span> <span class="token string">"^7.15.0"</span><span class="token punctuation">,</span>
    <span class="token property">"@nuxtjs/eslint-config"</span><span class="token operator">:</span> <span class="token string">"^6.0.1"</span><span class="token punctuation">,</span>
    <span class="token property">"@nuxtjs/eslint-module"</span><span class="token operator">:</span> <span class="token string">"^3.0.2"</span><span class="token punctuation">,</span>
    <span class="token property">"eslint"</span><span class="token operator">:</span> <span class="token string">"^7.32.0"</span><span class="token punctuation">,</span>
    <span class="token property">"eslint-config-prettier"</span><span class="token operator">:</span> <span class="token string">"^8.3.0"</span><span class="token punctuation">,</span>
    <span class="token property">"eslint-plugin-nuxt"</span><span class="token operator">:</span> <span class="token string">"^2.0.0"</span><span class="token punctuation">,</span>
    <span class="token property">"eslint-plugin-prettier"</span><span class="token operator">:</span> <span class="token string">"^3.4.0"</span><span class="token punctuation">,</span>
    <span class="token property">"eslint-plugin-vue"</span><span class="token operator">:</span> <span class="token string">"^7.15.1"</span><span class="token punctuation">,</span>
    <span class="token property">"prettier"</span><span class="token operator">:</span> <span class="token string">"^2.3.2"</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Configure ESLint in <code>.eslintrc.js</code> (in your project root):</p>
<div class="nuxt-content-highlight"><span class="filename">.eslintrc.js</span><pre class="line-numbers language-js"><code>module<span class="token punctuation">.</span><span class="token property-access">exports</span> <span class="token operator">=</span> <span class="token punctuation">{</span>
    <span class="token literal-property property">root</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
    <span class="token literal-property property">env</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token literal-property property">browser</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
        <span class="token literal-property property">node</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token literal-property property">parserOptions</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token literal-property property">parser</span><span class="token operator">:</span> <span class="token string">'@babel/eslint-parser'</span><span class="token punctuation">,</span>
        <span class="token literal-property property">requireConfigFile</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token keyword">extends</span><span class="token operator">:</span> <span class="token punctuation">[</span>
        <span class="token string">'@nuxtjs'</span><span class="token punctuation">,</span>
        <span class="token string">'plugin:nuxt/recommended'</span><span class="token punctuation">,</span>
        <span class="token string">'prettier'</span>
    <span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token literal-property property">plugins</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'prettier'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token literal-property property">rules</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token string-property property">'prettier/prettier'</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'error'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        <span class="token string-property property">'vue/html-indent'</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'error'</span><span class="token punctuation">,</span> <span class="token number">4</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        <span class="token string-property property">'vue/singleline-html-element-content-newline'</span><span class="token operator">:</span> <span class="token number">0</span><span class="token punctuation">,</span>
        <span class="token string-property property">'vue/component-name-in-template-casing'</span><span class="token operator">:</span> <span class="token punctuation">[</span><span class="token string">'error'</span><span class="token punctuation">,</span> <span class="token string">'PascalCase'</span><span class="token punctuation">]</span><span class="token punctuation">,</span>
        <span class="token string-property property">'vue/valid-v-slot'</span><span class="token operator">:</span> <span class="token punctuation">[</span>
            <span class="token string">'error'</span><span class="token punctuation">,</span>
            <span class="token punctuation">{</span>
                <span class="token literal-property property">allowModifiers</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
            <span class="token punctuation">}</span><span class="token punctuation">,</span>
        <span class="token punctuation">]</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token literal-property property">globals</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token literal-property property">_</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Configure Prettier in <code>.prettierrc</code> (again in your project root):</p>
<div class="nuxt-content-highlight"><span class="filename">.prettierrc</span><pre class="line-numbers language-json"><code><span class="token punctuation">{</span>
    <span class="token property">"semi"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
    <span class="token property">"singleQuote"</span><span class="token operator">:</span> <span class="token boolean">true</span><span class="token punctuation">,</span>
    <span class="token property">"tabWidth"</span><span class="token operator">:</span> <span class="token number">4</span><span class="token punctuation">,</span>
    <span class="token property">"printWidth"</span><span class="token operator">:</span> <span class="token number">120</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Also create <code>.prettierignore</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">.prettierignore</span><pre class="line-numbers language-ini"><code><span class="token comment"># Ignore artifacts:</span>
build
coverage
</code></pre></div>
<p>Configure <a href="https://code.visualstudio.com/" rel="nofollow noopener noreferrer" target="_blank">VS Code</a> <strong>Editor: Format on Save</strong> to apply auto formatting upon saving, which you can also restrict to specific languages, <code>.vscode/settings.json</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">.vscode/settings.json</span><pre class="line-numbers language-json"><code><span class="token punctuation">{</span>
    <span class="token property">"vetur.format.defaultFormatter.html"</span><span class="token operator">:</span> <span class="token string">"none"</span><span class="token punctuation">,</span>
    <span class="token comment">// Set the default</span>
    <span class="token property">"editor.formatOnSave"</span><span class="token operator">:</span> <span class="token boolean">false</span><span class="token punctuation">,</span>
    <span class="token comment">// Enable per-language</span>
    <span class="token property">"[javascript]"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token property">"editor.formatOnSave"</span><span class="token operator">:</span> <span class="token boolean">true</span>
    <span class="token punctuation">}</span><span class="token punctuation">,</span>
    <span class="token property">"[vue]"</span><span class="token operator">:</span> <span class="token punctuation">{</span>
        <span class="token property">"editor.formatOnSave"</span><span class="token operator">:</span> <span class="token boolean">true</span>
    <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Now, open any <code>.vue</code> or <code>.js</code> file, fire <code>Cmd-S</code> to save the file and see the magic. If it won't yet work for you, consider restarting VSCode.</p>
<p>Don't worry, a file only gets formatted if you explicitely hit <code>Cmd-S</code>. You can still use <code>autoSave</code> for regular saving, which would not invoke auto-formatting of the whole file. That's the relevant global VS Code user settings (can be found in <code>$HOME/Library/Application Support/Code/User/settings.json</code> on macOS):</p>
<div class="nuxt-content-highlight"><span class="filename">settings.json</span><pre class="line-numbers language-json"><code><span class="token punctuation">{</span>
    <span class="token property">"files.autoSave"</span><span class="token operator">:</span> <span class="token string">"afterDelay"</span><span class="token punctuation">,</span>
    <span class="token property">"prettier.printWidth"</span><span class="token operator">:</span> <span class="token number">81</span><span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Done. Happy clean coding!</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="frontend" term="frontend"/>
        <category label="vue.js" term="vue.js"/>
        <category label="nuxt.js" term="nuxt.js"/>
        <category label="js" term="js"/>
        <category label="vscode" term="vscode"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Restrict commands by SSH authorized_keys command option]]></title>
        <id>https://pipo.blog/articles/20210803-ssh-allowed-commands</id>
        <link href="https://pipo.blog/articles/20210803-ssh-allowed-commands"/>
        <updated>2021-08-03T16:00:00.000Z</updated>
        <summary type="html"><![CDATA[Limit SSH access to specific commands by the lovely command option in authorized_keys.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/ssh-key.png"></p>
                      <p>SSH <code>authorized_keys</code> allows you to define a command which is executed upon authentication with a specific key by prefixing it with the <code>command="cmd"</code> option.</p>
<p><code>authorized_keys</code> man page explains this wonderful feature as follows:</p>
<!--more-->
<blockquote>
<p><strong><code>command="command"</code></strong></p>
<p>Specifies that the command is executed whenever this key is used for authentication.  The command supplied by the user (if any) is ignored.
(...)
This option might be useful to restrict certain public keys to perform just a specific operation.  An example might be a key that permits remote backups but nothing else.
(...)
The command originally supplied by the client is available in the <code>SSH_ORIGINAL_COMMAND</code> environment variable.</p>
</blockquote>
<p>The syntax goes like this, put <code>command="cmd"</code> and all other options before a pubkey in <code>authorized_keys</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token assign-left variable">command</span><span class="token operator">=</span><span class="token string">"cmd"</span>,no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-rsa AAAAB3N<span class="token punctuation">..</span>.
</code></pre></div>
<p>A detailed description of these options can be found here:</p>
<ul>
<li><a href="https://www.ssh.com/academy/ssh/authorized_keys/openssh" rel="nofollow noopener noreferrer" target="_blank">Configuring authorized_keys for OpenSSH</a></li>
</ul>
<p>You might have missed the two use cases from my other blog posts, as they are quite long and complex:</p>
<ul>
<li><a href="/articles/20180306-simple-and-secure-external-backup">2018-03-06 Simple and Secure External Backup</a>: Restricting SSH access to rsync only from a specific directory</li>
<li><a href="/articles/20210803-secure-zfs-encrypted-extbackup">2021-08-03 Secure External Backup with ZFS Native Encryption</a>: Restricting SSH access to a list of specific command patterns</li>
</ul>
<p>So here's the two use cases in short:</p>
<h2 id="use-case-1-limit-rsync-to-a-directory-rrsync"><a href="#use-case-1-limit-rsync-to-a-directory-rrsync" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Use Case 1: Limit rsync to a directory (rrsync)</h2>
<p>Let's suppose we need full root permissions to pull data from a backup server, but its access should be limited to <code>rsync</code> from a specific directory and not executing any other commands on the remote <code>backup</code> server. The host that pulls data is going to be called <code>extbackup</code>.</p>
<p>There is a simple rsync wrapper for this which comes shipped with Debian's rsync package but first needs to be unpacked/installed:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>backup$ zcat /usr/share/doc/rsync/scripts/rrsync.gz <span class="token operator">></span> /usr/local/bin/rrsync
backup$ <span class="token function">chmod</span> 0755 /usr/local/bin/rrsync
backup$ <span class="token function">ln</span> <span class="token parameter variable">-s</span> /usr/local/bin/rrsync /usr/bin/rrsync
</code></pre></div>
<p>We then add this to <code>/etc/sudoers.d/extbackup</code> on <code>backup</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>%backuppers ALL= NOPASSWD:SETENV: /usr/bin/rrsync
</code></pre></div>
<p>Let's now create a specific user <code>extbackup</code> and add him to group <code>backuppers</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>backup$ adduser extbackup
backup$ adduser extbackup backuppers
</code></pre></div>
<p>Add the public key of <code>extbackup</code> (found in <code>extbackup:~/.ssh/id_rsa.pub</code>) to <code>backup:/home/extbackup/.ssh/authorized_keys</code>, prefixed by the following options:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token assign-left variable">command</span><span class="token operator">=</span><span class="token string">"sudo -E /usr/bin/rrsync -ro /backups/"</span>,no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-rsa AAAABxyz<span class="token punctuation">..</span>.
</code></pre></div>
<p>This limits <code>extbackup</code> to only pull data from <code>backup:/backups/</code> directory over rsync. Test-run:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>extbackup$ <span class="token function">rsync</span> <span class="token parameter variable">-aH</span> --numeric-ids <span class="token parameter variable">--delete</span> --delete-before extbackup@backup: /mnt/backups
</code></pre></div>
<blockquote>
<p>Explanation: <code>extbackup</code>'s root user rsyncs data from <code>backup:/backups/</code> to <code>/mnt/backups</code> but only needs to provide the relative path to <code>backup</code> as rsyncing is enforced over rrsync to <code>/backups/</code> directory on the remote side. If we want to pull all data from <code>backup:/backups/</code>, simply provide an empty relative path after the colon.</p>
</blockquote>
<p>Isn't that cool? We can pull data with root rights from our backup server but are limited to one specific directory and are not able to execute any other commands except rsync!</p>
<h2 id="use-case-2-restrict-to-specific-command-patterns"><a href="#use-case-2-restrict-to-specific-command-patterns" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Use Case 2: Restrict to specific command patterns</h2>
<p>As pointed out <a href="https://serverfault.com/a/803873/299863" rel="nofollow noopener noreferrer" target="_blank">here</a>, OpenSSH's <code>authorized_keys</code> command option enforces a single command (by overriding the original command). So the question was:</p>
<blockquote>
<p>The <code>authorized_keys</code> has a <code>command="..."</code> option that restricts a key to a single command. Is there a way to restrict a key to <em>multiple</em> commands? E.g. by having a regex there, or by editing some other configuration file?</p>
</blockquote>
<p>This can be simply accomplished by a wrapper script, as OpenSSH makes the original command supplied by the client available in the <code>SSH_ORIGINAL_COMMAND</code> environment variable.</p>
<p>In <a href="/articles/20210803-secure-zfs-encrypted-extbackup">Secure External Backup with ZFS Native Encryption</a> our requirement was to limit root access over SSH to only specific command patterns, so that the client (in this case <code>extbackup</code> which pulls data from <code>backup</code> server) was only able to execute commands which were needed for <a href="https://pve.proxmox.com/wiki/PVE-zsync" rel="nofollow noopener noreferrer" target="_blank">PVE-zsync</a> backup/replication.</p>
<p>To accomplish this specific restrictions, we need to use a script in <code>command</code> section before the <code>extbackup</code>'s pubkey in <code>.ssh/authorized_keys</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token assign-left variable">command</span><span class="token operator">=</span><span class="token string">"/root/.ssh/allowed-commands.sh"</span>,no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-rsa AAAAB3N<span class="token punctuation">..</span>. root@extbackup
</code></pre></div>
<p><code>allowed-commands.sh</code> then does pattern matching to verify the commands - using <code>$SSH_ORIGINAL_COMMAND</code> to get the original command:</p>
<blockquote>
<p>For numeric ranges in Bash regexes I have used <a href="https://3widgets.com/" rel="nofollow noopener noreferrer" target="_blank">Regex Numeric Range Generator</a>.</p>
</blockquote>
<div class="nuxt-content-highlight"><span class="filename">allowed-commands.sh</span><pre class="line-numbers language-bash"><code><span class="token shebang important">#!/bin/bash</span>

<span class="token comment">## CONFIGURATION ##############################################</span>
<span class="token assign-left variable">veid_pattern</span><span class="token operator">=</span><span class="token string">'(19[4-9]|2[0-4][0-9]|25[0-4])'</span>
<span class="token assign-left variable">backup_srcdir</span><span class="token operator">=</span><span class="token string">'/backup'</span>
<span class="token assign-left variable">pool_name</span><span class="token operator">=</span>dpool
<span class="token comment">###############################################################</span>

<span class="token comment"># Regex patterns</span>
<span class="token comment"># WARNING: '\w' is not supported in Bash, so we need to use '[:alnum:]' instead</span>
<span class="token assign-left variable">ds_pattern</span><span class="token operator">=</span><span class="token string">"<span class="token variable">${pool_name}</span>/zfsdisks/subvol-<span class="token variable">${veid_pattern}</span>-disk-1"</span>
<span class="token assign-left variable">ts_pattern</span><span class="token operator">=</span><span class="token string">'20[2-9][0-9]-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])_(0[0-9]|1[0-9]|2[0-3])(:(0[0-9]|[1-5][0-9])){2}'</span>
<span class="token assign-left variable">snap_prefix_pattern</span><span class="token operator">=</span><span class="token string">'rep_extbackup_([1-9]a_)?'</span>
<span class="token assign-left variable">snap_pattern</span><span class="token operator">=</span><span class="token string">"@<span class="token variable">${snap_prefix_pattern}</span><span class="token variable">${ts_pattern}</span>"</span>
<span class="token assign-left variable">ds_snap_pattern</span><span class="token operator">=</span><span class="token string">"<span class="token variable">${ds_pattern}</span><span class="token variable">${snap_pattern}</span>"</span>

<span class="token assign-left variable">patt1</span><span class="token operator">=</span><span class="token string">'zfs list -r( -)?t snapshot -Ho name( -S creation)? '</span><span class="token string">"<span class="token variable">$ds_pattern</span>(<span class="token variable">$snap_pattern</span>)?"</span>
<span class="token assign-left variable">patt2</span><span class="token operator">=</span><span class="token string">'zfs (snapshot|destroy) '</span><span class="token string">"<span class="token variable">$ds_snap_pattern</span>"</span>
<span class="token assign-left variable">patt3</span><span class="token operator">=</span><span class="token string">'zfs send '</span><span class="token string">"(-i <span class="token variable">$ds_snap_pattern</span> )?-- <span class="token variable">$ds_snap_pattern</span>"</span>
<span class="token assign-left variable">patt4</span><span class="token operator">=</span><span class="token string">"zfs rename <span class="token variable">$ds_snap_pattern</span> <span class="token variable">$ds_snap_pattern</span>"</span> <span class="token comment"># only used in extbackup-migrate-zfs-snaps.sh</span>

<span class="token assign-left variable">cmd</span><span class="token operator">=</span><span class="token string">"<span class="token variable">$SSH_ORIGINAL_COMMAND</span>"</span>

<span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token string">"<span class="token variable">$cmd</span>"</span> <span class="token operator">==</span> <span class="token string">'list-datasets'</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token comment"># special command to get a server list of all datasets and mountpoints</span>
    zfs list <span class="token parameter variable">-H</span> <span class="token parameter variable">-o</span> name,mountpoint,usedds <span class="token operator">|</span> <span class="token function">grep</span> <span class="token variable">$backup_srcdir</span>
    <span class="token builtin class-name">exit</span>
<span class="token keyword">elif</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">$cmd</span> <span class="token operator">=~</span> ^<span class="token variable">$patt1</span>$ <span class="token operator">||</span> <span class="token variable">$cmd</span> <span class="token operator">=~</span> ^<span class="token variable">$patt2</span>$ <span class="token operator">||</span> <span class="token variable">$cmd</span> <span class="token operator">=~</span> ^<span class="token variable">$patt3</span>$ <span class="token operator">||</span> <span class="token variable">$cmd</span> <span class="token operator">=~</span> ^<span class="token variable">$patt4</span>$ <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token variable">$SSH_ORIGINAL_COMMAND</span>
    <span class="token builtin class-name">exit</span>
<span class="token keyword">else</span>
    logger <span class="token string">"<span class="token variable"><span class="token variable">$(</span><span class="token function">basename</span> $0<span class="token variable">)</span></span> violation: <span class="token variable">$cmd</span>"</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"Access denied"</span>
    <span class="token builtin class-name">exit</span> <span class="token number">1</span>
<span class="token keyword">fi</span>
</code></pre></div>
<p>We can then test the magic <code>list-datasets</code> command to get a server list of all datasets and mountpoints:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>extbackup$ <span class="token function">ssh</span> backup list-datasets
</code></pre></div>
<p>And <code>pve-zsync</code> is able to execute remote commands like such:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ zfs list <span class="token parameter variable">-r</span> <span class="token parameter variable">-t</span> snapshot <span class="token parameter variable">-Ho</span> name <span class="token parameter variable">-S</span> creation dpool/zfsdisks/subvol-181-disk-1
$ zfs snapshot dpool/zfsdisks/subvol-181-disk-1@rep_extbackup_2021-07-12_23:15:53
$ zfs send -- dpool/zfsdisks/subvol-181-disk-1@rep_extbackup_2021-07-12_23:15:53
$ zfs destroy dpool/zfsdisks/subvol-181-disk-1@rep_extbackup_2021-07-12_23:13:13
</code></pre></div>
<p>Whenever we try to fire another command, we get an <code>Access denied</code> and the command is logged to syslog.</p>
<p>Sidenote: I trust <code>extbackup</code> as a client, especially since its SSH private key is encrypted and is only getting decrypted to volatile memory. But I still wanted to restrict it to a small subset of commands, and also to a subset of ZFS datasets it can pull data from, for improved security.</p>
<p>OpenSSH's <code>authorized_keys</code> command option gives us full flexibility with such a wrapper script!</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="security" term="security"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Secure External Backup with ZFS Native Encryption]]></title>
        <id>https://pipo.blog/articles/20210803-secure-zfs-encrypted-extbackup</id>
        <link href="https://pipo.blog/articles/20210803-secure-zfs-encrypted-extbackup"/>
        <updated>2021-08-03T12:00:00.000Z</updated>
        <summary type="html"><![CDATA[How to set up a secure offsite and offline backup server solution based on snapshots and ZFS native encryption.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/samsung-ssd-870-qvo-1tb.jpg"></p>
                      <p>Let's improve our <a href="/articles/20180306-simple-and-secure-external-backup">Simple and Secure External Backup</a> solution I have published back in 2018. Back then, I was using <code>rsync</code> over SSH to pull backup data, and <code>LUKS</code> encryption as full disk encryption for the external drives. As we all know, transferring data with rsync can get horribly slow and blow up your I/O if you're transferring millions of small files. Also, <code>LUKS</code> encryption may be a bit low level and inflexible. What we want to accomplish: A performant and secure backup solution based on <strong>ZFS</strong>, using <code>zfs send|recv</code> for efficient data transfer, and ZFS native encryption to secure our external drives. So let's go ahead and built that thing from scratch on a fresh 2021 stack!</p>
<!--more-->
<p>Let's assume you already have an existing backup server that is connected to the internet 24/7 and runs daily/weekly/monthly backup jobs. Backup data is stored in ZFS datasets, ideally as individual datasets per full-system backup for each host. We would now like to set up a second offsite backup server that just cares about storing data to an encrypted external drive and after each backup run, you are going to physically detach that drive.</p>
<p>So, we are talking about offline backups in addition to the fact having this server offsite - at a different location than your main backup server.</p>
<p>Preferably, your main backup server would also be offsite. But as it needs to pull data frequently, its storage is always available and not getting detached.</p>
<p>Let's call your main backup server <code>backup</code> and the one we are going to set up here <code>extbackup</code>.</p>
<h2 id="requirements"><a href="#requirements" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Requirements</h2>
<p>We have the following – from a security point of view – rather strict requirements:</p>
<ul>
<li>Hardware: Payable but <strong>reliable fanless system</strong> that could be mounted to any office desk.</li>
<li>System: <strong><a href="https://proxmox.com/" rel="nofollow noopener noreferrer" target="_blank">Proxmox VE</a></strong> for well-maintained <a href="https://openzfs.org" rel="nofollow noopener noreferrer" target="_blank">OpenZFS</a> and <a href="https://pve.proxmox.com/wiki/PVE-zsync" rel="nofollow noopener noreferrer" target="_blank">PVE-zsync</a> support</li>
<li><strong>No backup data should ever be stored (not even temporarily!) on rpool (system partition), only on encrypted external drives.</strong></li>
<li>System is installed on internal SSD, external drives attached over USB 3 or eSATA</li>
<li>External drives fully encrypted using <strong>ZFS native encryption</strong> (preferred over LUKS for better handling and easier setup)</li>
<li><strong>ZFS encryption key</strong> not stored anywhere on disk, <strong>only generated at runtime</strong>.</li>
<li><strong>Snapshot-based backup</strong> for optimal performance: Backup data is pulled from <code>backup</code> over SSH, using <code>zfs send|recv</code>. We choose <strong><a href="https://pve.proxmox.com/wiki/PVE-zsync" rel="nofollow noopener noreferrer" target="_blank">PVE-zsync</a></strong> as simple backup tool / replication manager</li>
<li><strong>SSH private key</strong> to connect to <code>backup</code> is not stored anywhere on <code>extbackup</code> in plaintext, <strong>only getting generated at runtime</strong>.</li>
<li><strong>SSH access</strong> on remote <code>backup</code> server is <strong>strictly limited to <code>pve-zsync</code> operations</strong>, without providing a full shell.</li>
<li><strong>Both ZFS encryption key and SSH private key should not be required to be stored in any password manager or anywhere else.</strong></li>
<li>Unlock password for those keys should only be used as a 2nd factor / seed, so <strong>encryption security does not depend on its length</strong>.</li>
<li>External disks should only get decrypted during a backup run and getting encrypted/unmounted right afterwards.</li>
<li>Backup data is directly streamed to external disks without temporary storage on <code>extbackup</code> server.</li>
<li>Ideally, the external drive should be a plain SATA drive without external casing, which could be plugged directly into a server via onboard SATA or eSATA (in case of an emergency when we lack time to transfer the data for recovery). Performance should also be decent in case we ever run from such a drive in production, so SSD is preferred!</li>
</ul>
<p>Phew! This sounds like a lot of tight requirements! But it is all doable in a very simple slick and sexy setup. Keep on reading!</p>
<h2 id="hardware-setup"><a href="#hardware-setup" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Hardware Setup</h2>
<p>I do recommend the following hardware for a small robust fanless system that is built for 24/7 operation:</p>





















<table><thead><tr><th>component</th><th>spec</th></tr></thead><tbody><tr><td>System</td><td><a href="http://global.shuttle.com/products/productsDetail?productId=2403" rel="nofollow noopener noreferrer" target="_blank">Shuttle Barebone XPC slim <strong>DS10U</strong> Series</a> (DS10U5 / Intel Core i5-8265U)</td></tr><tr><td>RAM</td><td>2x SO-DDR4-RAM Vengeance 2400MHz 16GB (CMSX16GX4M1A2400C16)</td></tr><tr><td>SSD</td><td>Samsung SSD 860 EVO AHCI (SATA) M.2 2280 250GB (MZ-N6E250BW)</td></tr></tbody></table>
<p>This hardware currently (Aug 2021) is available for ~ $900 in total. You can go much cheaper with some Intel NUC system that comes delivered with a basic RAM / SSD setup. But we do recommend that Shuttle XPC line as it is an industry-grade platform and we never had any issues with its predecessors DS57U / DS77U. It's also 100% quiet, producing no noise at all.</p>
<p>And maybe you want to save some bucks and don't get the powerful DS10U5 with an i5, but rather a DS10U3 with an i3 or even the base model with a Celeron 4205U.</p>
<p>As external 8TB SSD (we're up for speed!), I do recommend the following:</p>
<ul>
<li><strong>Samsung 870 QVO</strong> 8TB (model <a href="https://www.samsung.com/ch/memory-storage/sata-ssd/ssd-870-qvo-sata-3-2-5-inch-8tb-mz-77q8t0bw/" rel="nofollow noopener noreferrer" target="_blank">MZ-77Q8T0BW</a>), or go for 4/2/1 TB variant, if that is enough for you</li>
<li><strong>StarTech USB 3.1 to SATA adapter</strong> (model <a href="https://www.startech.com/en-ch/hdd/usb312sat3cb" rel="nofollow noopener noreferrer" target="_blank">USB312SAT3CB</a>)</li>
</ul>
<p>Do <strong>NOT</strong> use any USB3-to-SATA adapter that emulates its own device, e.g. this shitty piece of crap: <a href="https://www.digitec.ch/de/s1/product/manhattan-usb-30-auf-sata-7-pin-sata-7-pin-280cm-data-video-adapter-13969552" rel="nofollow noopener noreferrer" target="_blank">Manhattan USB 3.0 to SATA</a> (model 130424). The adapter should support direct access to your drive. The StarTech adapter supports <strong><a href="https://en.wikipedia.org/wiki/USB_Attached_SCSI" rel="nofollow noopener noreferrer" target="_blank">UASP (USB Attached SCSI)</a></strong> which is what we want!</p>
<p>This whole hardware setup might sound luxury in your eyes. It all depends of how important the (customer) data is and how far you want to optimize disaster recovery time. You can go much cheaper if that all doesn't matter to you.</p>
<p>These are my recommendations and they have been very well tested at <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime GmbH</a> webhosting.</p>
<h2 id="system-setup"><a href="#system-setup" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>System Setup</h2>
<p>Install <strong><a href="https://proxmox.com/en/proxmox-ve" rel="nofollow noopener noreferrer" target="_blank">Proxmox VE 7</a></strong> (Debian Bullseye based) from an USB stick which you create as follows:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">wget</span> https://www.proxmox.com/images/download/pve/iso/proxmox-ve_7.0-1.iso
$ <span class="token function">cat</span> proxmox-ve_7.0-1.iso <span class="token operator">></span> /dev/sdx
</code></pre></div>
<p>On macOS, use <code>diskutil list</code> to identify the correct device name of your USB stick and run <code>diskutil unmountDisk /dev/disk2</code> prior to writing to it. Something like:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ diskutil list
$ <span class="token function">sudo</span> <span class="token function">su</span> -
$ diskutil unmountDisk /dev/disk2
$ <span class="token function">cat</span> proxmox-ve_7.0-1.iso <span class="token operator">></span> /dev/disk2
</code></pre></div>
<p>I am not going to explain how to set up Proxmox VE here as its installer is super slick and I am sure you are going to manage that by yourself.</p>
<h2 id="build-encryption-key"><a href="#build-encryption-key" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Build encryption key</h2>
<p>As promised, the encryption key for ZFS native encryption (and a second LUKS encryption key, if we're ever going to use that again) is only going to be stored at runtime in <strong>ramfs</strong> (volatile memory aka. RAM). It is built upon first login as root. So let's hook below script into <code>/root/.profile</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token builtin class-name">echo</span> <span class="token string">"/usr/local/sbin/build-encryption-key.sh"</span> <span class="token operator">>></span> /root/.profile
</code></pre></div>
<p>Deploy the following script to <code>/usr/local/sbin/build-encryption-key.sh</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">build-encryption-key.sh</span><pre class="line-numbers language-bash"><code><span class="token shebang important">#!/bin/bash</span>
<span class="token comment">#</span>
<span class="token comment"># This script usually is called on the first login and asks for a password</span>
<span class="token comment"># to build the LUKS + ZFS encryption keys which then are stored only in volatile</span>
<span class="token comment"># memory /mnt/ramfs (ramfs).</span>
<span class="token comment"># This script is added to your /root/.profile in order you won't forget to</span>
<span class="token comment"># build the encryption key each time you reboot the server.</span>
<span class="token comment">#</span>
<span class="token comment"># We are using ramfs instead of tmpfs as there is no swapping support in </span>
<span class="token comment"># ramfs which is good in a security perspective.</span>
<span class="token comment"># see: http://www.thegeekstuff.com/2008/11/overview-of-ramfs-and-tmpfs-on-linux</span>
<span class="token comment">#</span>

<span class="token comment">################### CONFIGURATION #######################</span>
<span class="token assign-left variable">RAMFS_PATH</span><span class="token operator">=</span><span class="token string">'/mnt/ramfs'</span>
<span class="token assign-left variable">RAMFS_SIZE</span><span class="token operator">=</span><span class="token string">'20M'</span>
<span class="token assign-left variable">LUKS_KEYFILE</span><span class="token operator">=</span><span class="token variable">$RAMFS_PATH</span>/luks_pw
<span class="token assign-left variable">ZFS_KEYFILE</span><span class="token operator">=</span><span class="token variable">$RAMFS_PATH</span>/zfs_enc_key
<span class="token assign-left variable">SALT</span><span class="token operator">=</span><span class="token string">'**************************************************'</span>
<span class="token assign-left variable">SHA1_CHECK</span><span class="token operator">=</span><span class="token string">'fb9e740efe20f541349d37eff7aa34efd4ac823d'</span>
<span class="token comment">#########################################################</span>

<span class="token function-name function">printinfo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"[INFO] <span class="token variable">$1</span>"</span>
<span class="token punctuation">}</span>

<span class="token function-name function">printwarn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"[WARNING] <span class="token variable">$1</span>"</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">--color</span> <span class="token string">"WARNING"</span>
<span class="token punctuation">}</span>

<span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token parameter variable">-f</span> <span class="token string">"<span class="token variable">$LUKS_KEYFILE</span>"</span> <span class="token operator">&#x26;&#x26;</span> <span class="token parameter variable">-f</span> <span class="token string">"<span class="token variable">$ZFS_KEYFILE</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token comment"># exit silently, as the key file already exists</span>
    <span class="token builtin class-name">exit</span>
<span class="token keyword">else</span>
    printwarn <span class="token string">"The LUKS (<span class="token variable">$LUKS_KEYFILE</span>) and ZFS (<span class="token variable">$ZFS_KEYFILE</span>) key files don't yet exist!"</span>
<span class="token keyword">fi</span>

<span class="token comment"># set up RAM disk if it is not yet mounted</span>
<span class="token keyword">if</span> <span class="token operator">!</span> mountpoint <span class="token parameter variable">-q</span> <span class="token string">"<span class="token variable">$RAMFS_PATH</span>"</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"Setting up ramfs on <span class="token variable">$RAMFS_PATH</span> (size=<span class="token variable">$RAMFS_SIZE</span>) ..."</span>
    <span class="token function">mkdir</span> <span class="token parameter variable">-p</span> <span class="token variable">$RAMFS_PATH</span>
    <span class="token function">mount</span> <span class="token parameter variable">-t</span> ramfs <span class="token parameter variable">-o</span> <span class="token assign-left variable">size</span><span class="token operator">=</span><span class="token variable">$RAMFS_SIZE</span> ramfs <span class="token variable">$RAMFS_PATH</span>
    <span class="token keyword">if</span> <span class="token operator">!</span> <span class="token function">grep</span> <span class="token parameter variable">-q</span> <span class="token string">"<span class="token variable">$RAMFS_PATH</span>"</span> /etc/fstab<span class="token punctuation">;</span> <span class="token keyword">then</span>
        <span class="token builtin class-name">echo</span> <span class="token string">"Adding line to /etc/fstab to persist mounting of <span class="token variable">$RAMFS_PATH</span> ..."</span>
        <span class="token builtin class-name">echo</span> <span class="token string">"ramfs   <span class="token variable">$RAMFS_PATH</span>              ramfs   defaults,size=<span class="token variable">$RAMFS_SIZE</span>        0 0"</span> <span class="token operator">>></span> /etc/fstab
    <span class="token keyword">fi</span>
<span class="token keyword">fi</span>

<span class="token comment"># make this script start on each login</span>
<span class="token assign-left variable">SCRIPT</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span>readlink <span class="token parameter variable">-f</span> $0<span class="token variable">)</span></span>
<span class="token keyword">if</span> <span class="token operator">!</span> <span class="token function">grep</span> <span class="token parameter variable">-q</span> <span class="token string">"<span class="token variable">$SCRIPT</span>"</span> /root/.profile<span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"<span class="token variable">$SCRIPT</span>"</span> <span class="token operator">>></span> /root/.profile
<span class="token keyword">fi</span>

<span class="token comment"># get password from interactive user input</span>
<span class="token keyword">while</span> <span class="token builtin class-name">read</span> <span class="token parameter variable">-s</span> <span class="token parameter variable">-p</span> <span class="token string">'Unlock encryption keys: '</span> PASS <span class="token operator">&#x26;&#x26;</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable"><span class="token variable">$(</span><span class="token builtin class-name">echo</span> <span class="token parameter variable">-n</span> <span class="token string">"<span class="token variable">$PASS</span>"</span> <span class="token operator">|</span> <span class="token function">wc</span> <span class="token parameter variable">--chars</span><span class="token variable">)</span></span> <span class="token parameter variable">-lt</span> <span class="token number">8</span> <span class="token punctuation">]</span><span class="token punctuation">]</span> <span class="token punctuation">;</span> <span class="token keyword">do</span>
    <span class="token builtin class-name">echo</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"Your password must be at least 8 characters long!"</span>
<span class="token keyword">done</span>
<span class="token builtin class-name">echo</span>

<span class="token comment"># calculate encryption key (SHA-512 hash of salt.password concatenation)</span>
<span class="token assign-left variable">KEY</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token builtin class-name">echo</span> <span class="token parameter variable">-n</span> <span class="token string">"<span class="token variable">$SALT</span>.<span class="token variable">$PASS</span>"</span> <span class="token operator">|</span> sha512sum <span class="token operator">|</span> <span class="token function">cut</span> -d<span class="token string">' '</span> <span class="token parameter variable">-f1</span><span class="token variable">)</span></span>

<span class="token comment"># store LUKS key file to ramfs</span>
<span class="token function">touch</span> <span class="token variable">$LUKS_KEYFILE</span> <span class="token operator">&#x26;&#x26;</span> <span class="token function">chmod</span> <span class="token number">600</span> <span class="token variable">$LUKS_KEYFILE</span>
<span class="token builtin class-name">echo</span> <span class="token parameter variable">-n</span> <span class="token string">"<span class="token variable">$KEY</span>"</span> <span class="token operator">></span> <span class="token variable">$LUKS_KEYFILE</span>

<span class="token comment"># SHA-1 check of the key - assure you have correctly built it by entering the correct password</span>
<span class="token assign-left variable">KEY_SHA1</span><span class="token operator">=</span><span class="token variable"><span class="token variable">`</span><span class="token function">cat</span> $LUKS_KEYFILE <span class="token operator">|</span> sha1sum <span class="token operator">|</span> <span class="token function">cut</span> -d<span class="token string">' '</span> <span class="token parameter variable">-f1</span><span class="token variable">`</span></span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token string">"<span class="token variable">$KEY_SHA1</span>"</span> <span class="token operator">!=</span> <span class="token string">"<span class="token variable">$SHA1_CHECK</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    printwarn <span class="token string">"Your key does not seem to be correct. You might have entered the wrong password. Please run <span class="token variable"><span class="token variable">`</span><span class="token function">basename</span> $SCRIPT<span class="token variable">`</span></span> again!"</span>
    printinfo <span class="token string">"If you are sure you have entered the right password, try to set SHA1_CHECK='<span class="token variable">$KEY_SHA1</span>' in <span class="token variable"><span class="token variable">`</span><span class="token function">basename</span> $SCRIPT<span class="token variable">`</span></span>."</span>
    <span class="token function">rm</span> <span class="token parameter variable">-f</span> <span class="token variable">$LUKS_KEYFILE</span>
    <span class="token builtin class-name">exit</span> <span class="token number">1</span>
<span class="token keyword">fi</span>

printinfo <span class="token string">"The LUKS key file was successfully stored in <span class="token variable">$LUKS_KEYFILE</span>."</span>

<span class="token comment"># ZFS raw key needs to be 32 characters long</span>
<span class="token function">head</span> <span class="token parameter variable">-c</span> <span class="token number">32</span> <span class="token variable">$LUKS_KEYFILE</span> <span class="token operator">></span> <span class="token variable">$ZFS_KEYFILE</span>
printinfo <span class="token string">"The ZFS key file was successfully stored in <span class="token variable">$ZFS_KEYFILE</span>."</span>

<span class="token comment">############################</span>
<span class="token comment"># SSH private key encryption</span>
<span class="token comment">############################</span>
<span class="token comment">#</span>
<span class="token comment"># INITIAL SETUP:</span>
<span class="token comment">#</span>
<span class="token comment"># Initially, we need to create an encrypted version of id_rsa and destroy the plaintext private key:</span>
<span class="token comment">#   $ cat ~/.ssh/id_rsa | openssl enc -e -aes-256-cbc -pbkdf2 -a -pass file:/mnt/ramfs/luks_pw > ~/.ssh/id_rsa.encrypted</span>
<span class="token comment">#</span>
<span class="token comment"># or you could also copy the password from /mnt/ramfs/luks_pw and enter it interactively:</span>
<span class="token comment">#   $ cat ~/.ssh/id_rsa | openssl enc -e -aes-256-cbc -pbkdf2 -a > ~/.ssh/id_rsa.encrypted</span>
<span class="token comment">#   $ enter aes-256-cbc encryption password: (...)</span>
<span class="token comment">#   $ chmod 600 ~/.ssh/id_rsa.encrypted</span>
<span class="token comment">#   $ shred -u ~/.ssh/id_rsa</span>
<span class="token comment">#</span>
<span class="token comment"># Updating the encrypted version of id_rsa:</span>
<span class="token comment">#   $ cat /mnt/ramfs/id_rsa.decrypted | openssl enc -e -aes-256-cbc -pbkdf2 -a -pass file:/mnt/ramfs/luks_pw > ~/.ssh/id_rsa.encrypted</span>
<span class="token comment"># Testing encryption and decryption:</span>
<span class="token comment">#   $ cat /mnt/ramfs/id_rsa.decrypted | openssl enc -e -aes-256-cbc -pbkdf2 -a -pass file:/mnt/ramfs/luks_pw > ~/.ssh/id_rsa.encrypted-testing</span>
<span class="token comment">#   $ cat ~/.ssh/id_rsa.encrypted-testing | openssl base64 -d | openssl enc -d -aes-256-cbc -pbkdf2 -pass file:/mnt/ramfs/luks_pw > /mnt/ramfs/id_rsa.testing</span>
<span class="token comment">#   $ diff /mnt/ramfs/id_rsa.decrypted /mnt/ramfs/id_rsa.testing</span>
<span class="token comment">#   $ shred -u /mnt/ramfs/id_rsa.testing</span>
<span class="token comment">#</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token parameter variable">-f</span> ~/.ssh/id_rsa.encrypted <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token function">touch</span> <span class="token variable">$RAMFS_PATH</span>/id_rsa.decrypted <span class="token operator">&#x26;&#x26;</span> <span class="token function">chmod</span> <span class="token number">600</span> <span class="token variable">$RAMFS_PATH</span>/id_rsa.decrypted
    <span class="token function">cat</span> ~/.ssh/id_rsa.encrypted <span class="token operator">|</span> openssl base64 <span class="token parameter variable">-d</span> <span class="token operator">|</span> openssl enc <span class="token parameter variable">-d</span> -aes-256-cbc <span class="token parameter variable">-pbkdf2</span> <span class="token parameter variable">-pass</span> file:<span class="token variable">$LUKS_KEYFILE</span> <span class="token operator">></span> <span class="token variable">$RAMFS_PATH</span>/id_rsa.decrypted
    <span class="token function">ln</span> <span class="token parameter variable">-sf</span> <span class="token variable">$RAMFS_PATH</span>/id_rsa.decrypted ~/.ssh/id_rsa
    printinfo <span class="token string">"SSH private key was successfully decrypted to <span class="token variable">$RAMFS_PATH</span>/id_rsa.decrypted."</span>
<span class="token keyword">else</span>
    printwarn <span class="token string">"Please encrypt your SSH private key to ~/.ssh/id_rsa.encrypted so I can decrypt it to ramfs."</span>
<span class="token keyword">fi</span>
</code></pre></div>
<p>But before using this script in production, <strong>ensure you have set your own <code>SALT</code> in the CONFIGURATION section</strong>. Don't yet care about <code>SHA1_CHECK</code> - this is only used to verify that the key got correctly built. Upon first run, the script will provide you with the right information. The unlock password can be freely chosen, just please remember it!:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ build-encryption-key.sh
Unlock encryption keys: <span class="token punctuation">(</span>choose a new unlock password<span class="token punctuation">)</span>
<span class="token punctuation">[</span>WARNING<span class="token punctuation">]</span> Your key does not seem to be correct. You might have entered the wrong password. Please run build-encryption-key.sh again<span class="token operator">!</span>
<span class="token punctuation">[</span>INFO<span class="token punctuation">]</span> If you are sure you have entered the right password, try to <span class="token builtin class-name">set</span> <span class="token assign-left variable">SHA1_CHECK</span><span class="token operator">=</span><span class="token string">'fb9e740efe20f541349d37eff7aa34efd4ac823d'</span> <span class="token keyword">in</span> build-encryption-key.sh.
</code></pre></div>
<blockquote>
<p>The unlock password is only needed to unlock/generate your LUKS + ZFS encryption keys and is only used as a second factor. For ZFS native encryption, we are then only going to use the generated <code>/mnt/ramfs/zfs_enc_key</code> which you should not store anywhere else!!!</p>
</blockquote>
<p>Set correct <code>SHA1_CHECK</code> (from above output) in <code>/usr/local/sbin/build-encryption-key.sh</code> and test if the script gets correctly invoked upon first login as root:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>extbackup$ <span class="token function">sudo</span> <span class="token function">su</span> -
<span class="token punctuation">[</span>WARNING<span class="token punctuation">]</span> The LUKS <span class="token punctuation">(</span>/mnt/ramfs/luks_pw<span class="token punctuation">)</span> and ZFS <span class="token punctuation">(</span>/mnt/ramfs/zfs_enc_key<span class="token punctuation">)</span> key files don't yet exist<span class="token operator">!</span>
Unlock encryption keys: 
<span class="token punctuation">[</span>INFO<span class="token punctuation">]</span> The LUKS key <span class="token function">file</span> was successfully stored <span class="token keyword">in</span> /mnt/ramfs/luks_pw.
<span class="token punctuation">[</span>INFO<span class="token punctuation">]</span> The ZFS key <span class="token function">file</span> was successfully stored <span class="token keyword">in</span> /mnt/ramfs/zfs_enc_key.
</code></pre></div>
<h2 id="set-up-ssh-keypair-with-encrypted-private-key"><a href="#set-up-ssh-keypair-with-encrypted-private-key" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Set up SSH keypair with encrypted private key</h2>
<p>You may have noticed that our <code>build-encryption-key.sh</code> script also tries to decrypt your SSH private key to ramfs upon first login as root. We are going to use the default identity file <code>~/.ssh/id_rsa</code> to access remote <code>backup</code> server over SSH (using <code>pve-zsync</code> to pull data). But we don't want the private key to get stored plaintext anywhere on persistent storage.</p>
<p><code>build-encryption-key.sh</code> cares about the following:</p>
<ul>
<li>Asks for encryption key password</li>
<li>Builds LUKS encryption key <code>/mnt/ramfs/luks_pw</code> using a seed and the provided password</li>
<li>Derives ZFS encryption key <code>/mnt/ramfs/zfs_enc_key</code> from LUKS encryption key (first 32 chars)</li>
<li>Decrypts the encrypted SSH private key of user root <code>~/.ssh/id_rsa.encrypted</code> to <code>/mnt/ramfs/id_rsa.decrypted</code></li>
</ul>
<p>Initially, you need to do this in two steps, as we did not yet encrypt <code>id_rsa</code> private key yet.</p>
<blockquote>
<p>Also see setup instructions in comments inside <code>build-encryption-key.sh</code>!</p>
</blockquote>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">ssh</span> extbackup
$ <span class="token function">sudo</span> <span class="token function">su</span> -
<span class="token punctuation">[</span>WARNING<span class="token punctuation">]</span> The LUKS <span class="token punctuation">(</span>/mnt/ramfs/luks_pw<span class="token punctuation">)</span> and ZFS <span class="token punctuation">(</span>/mnt/ramfs/zfs_enc_key<span class="token punctuation">)</span> key files don<span class="token string">'t yet exist!
Unlock encryption keys: 
[INFO] The LUKS key file was successfully stored in /mnt/ramfs/luks_pw.
[INFO] The ZFS key file was successfully stored in /mnt/ramfs/zfs_enc_key.
[WARNING] Please encrypt your SSH private key to ~/.ssh/id_rsa.encrypted so I can decrypt it to ramfs.

# regenerate RSA/4096 keypair, if not already done
$ ssh-keygen -t rsa -b 4096 -P '</span>'

<span class="token comment"># encrypt SSH private key</span>
$ <span class="token builtin class-name">cd</span> /root/.ssh/
$ <span class="token function">cat</span> id_rsa <span class="token operator">|</span> openssl enc <span class="token parameter variable">-e</span> -aes-256-cbc <span class="token parameter variable">-pbkdf2</span> <span class="token parameter variable">-a</span> <span class="token parameter variable">-pass</span> file:/mnt/ramfs/luks_pw <span class="token operator">></span> id_rsa.encrypted
$ <span class="token function">chmod</span> <span class="token number">600</span> id_rsa.encrypted
</code></pre></div>
<p>Let's now secure erase our plaintext private key:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ shred <span class="token parameter variable">-u</span> /root/.ssh/id_rsa
</code></pre></div>
<p>Testing, logout and login again:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">rm</span> <span class="token parameter variable">-f</span> /mnt/ramfs/*
$ <span class="token builtin class-name">logout</span>
$ <span class="token function">sudo</span> <span class="token function">su</span> -
<span class="token punctuation">[</span>WARNING<span class="token punctuation">]</span> The LUKS key <span class="token function">file</span> <span class="token punctuation">(</span>/mnt/ramfs/luks_pw<span class="token punctuation">)</span> does not yet exist<span class="token operator">!</span>
Unlock LUKS encryption key: **********
<span class="token punctuation">[</span>INFO<span class="token punctuation">]</span> The LUKS key <span class="token function">file</span> was successfully stored <span class="token keyword">in</span> /mnt/ramfs/luks_pw.
<span class="token punctuation">[</span>INFO<span class="token punctuation">]</span> The ZFS key <span class="token function">file</span> was successfully stored <span class="token keyword">in</span> /mnt/ramfs/zfs_enc_key.
<span class="token punctuation">[</span>INFO<span class="token punctuation">]</span> SSH private key was successfully decrypted to /mnt/ramfs/id_rsa.decrypted.
</code></pre></div>
<p>Your keypair is now ready. You can now copy-paste your public key to remote <code>backup</code> server:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">cat</span> /root/.ssh/id_rsa.pub
</code></pre></div>
<p>Append it to <code>/root/.ssh/authorized_keys</code> on <code>backup</code> server.</p>
<h2 id="encrypt-external-drive-setup-dpool"><a href="#encrypt-external-drive-setup-dpool" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Encrypt external drive: Setup dpool</h2>
<ul>
<li><a href="https://docs.oracle.com/cd/E53394_01/html/E54801/gkkih.html" rel="nofollow noopener noreferrer" target="_blank">Encrypting ZFS File Systems</a>
<ul>
<li><a href="https://docs.oracle.com/cd/E53394_01/html/E54801/gkkih.html#SVZFSgkkuc" rel="nofollow noopener noreferrer" target="_blank">Examples of Encrypting ZFS File Systems</a></li>
<li><a href="https://blog.heckel.io/2017/01/08/zfs-encryption-openzfs-zfs-on-linux" rel="nofollow noopener noreferrer" target="_blank">How-To: Using ZFS Encryption at Rest in OpenZFS (ZFS on Linux, ZFS on FreeBSD, …)</a></li>
</ul>
</li>
</ul>
<p><strong>NOTE:</strong> We only need to set up the first external drive. To set up further external drives, use mirroring as described in the final chapter below.</p>
<p><strong>QUICK HOWTO:</strong> Initialize a new external drive <code>/dev/sdb</code></p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token comment"># encrypt disk / set up dpool (use -f to force, if dpool already exists)</span>
$ zpool create <span class="token parameter variable">-O</span> <span class="token assign-left variable">acltype</span><span class="token operator">=</span>posixacl <span class="token parameter variable">-O</span> <span class="token assign-left variable">encryption</span><span class="token operator">=</span>on <span class="token parameter variable">-O</span> <span class="token assign-left variable">keylocation</span><span class="token operator">=</span>file:///mnt/ramfs/zfs_enc_key <span class="token parameter variable">-O</span> <span class="token assign-left variable">keyformat</span><span class="token operator">=</span>raw dpool sdb
<span class="token comment"># check status</span>
$ zfs get encryption,keylocation,keystatus,keyformat dpool

<span class="token comment"># set up LABEL and dataset(s)</span>
$ <span class="token builtin class-name">echo</span> <span class="token parameter variable">-n</span> <span class="token string">"1a"</span> <span class="token operator">></span> /dpool/LABEL
$ zfs create dpool/zfsdisks

<span class="token comment"># unmount and export (only disconnect disk, after exporting dpool!)</span>
$ zfs unmount dpool <span class="token operator">&#x26;&#x26;</span> zfs unload-key dpool
$ zpool <span class="token builtin class-name">export</span> dpool
</code></pre></div>
<p>In more detail...</p>
<p>Initialize a new external drive with ZFS native encryption, putting everything under encrypted pool <code>dpool</code> (name stands for "data-pool", while <code>rpool</code> is the default system "root-pool" in Proxmox VE):</p>
<blockquote>
<p>The <code>aes-256-ccm</code> algorithm is used by default (don't be confused about misleading ZFS documentation which says <code>aes-128-ccm</code>)</p>
</blockquote>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token comment"># use lsblk to check device names</span>
$ lsblk

<span class="token comment"># create dpool on /dev/sdb</span>
<span class="token comment"># NOTE: key must be exactly 32 characters long, so use /mnt/ramfs/zfs_enc_key (first 32 chars of luks_pw)</span>
$ zpool create <span class="token parameter variable">-O</span> <span class="token assign-left variable">acltype</span><span class="token operator">=</span>posixacl <span class="token parameter variable">-O</span> <span class="token assign-left variable">encryption</span><span class="token operator">=</span>on <span class="token parameter variable">-O</span> <span class="token assign-left variable">keylocation</span><span class="token operator">=</span>file:///mnt/ramfs/zfs_enc_key <span class="token parameter variable">-O</span> <span class="token assign-left variable">keyformat</span><span class="token operator">=</span>raw dpool sdb

<span class="token comment"># check pool and encrytion status</span>
$ zpool status dpool
  pool: dpool
 state: ONLINE
config:
    NAME        STATE     READ WRITE CKSUM
    dpool       ONLINE       <span class="token number">0</span>     <span class="token number">0</span>     <span class="token number">0</span>
      sdb       ONLINE       <span class="token number">0</span>     <span class="token number">0</span>     <span class="token number">0</span>

$ zfs get encryption,keylocation,keystatus,keyformat dpool
NAME   PROPERTY     VALUE                          SOURCE
dpool  encryption   aes-256-gcm                    -
dpool  keylocation  file:///mnt/ramfs/zfs_enc_key  <span class="token builtin class-name">local</span>
dpool  keystatus    available                      -
dpool  keyformat    raw                            -
</code></pre></div>
<p><strong>Unmount/encrypt</strong> dpool like this:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ zfs unmount dpool
$ zfs unload-key dpool
$ zpool <span class="token builtin class-name">export</span> dpool
</code></pre></div>
<blockquote>
<p>WARNING: Never forget to export <code>dpool</code> before you detach the external drive or before you reboot!</p>
</blockquote>
<p><strong>Mount/decrypt</strong> <code>dpool</code> like this:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ zpool <span class="token function">import</span> dpool
$ zfs load-key dpool
$ zfs <span class="token function">mount</span> <span class="token parameter variable">-l</span> <span class="token parameter variable">-a</span>
</code></pre></div>
<blockquote>
<p>man zfs:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>zfs mount [-Olv] [-o options] -a | filesystem
 -a  Mount all available ZFS file systems.  Invoked automatically as
     part of the boot process if configured.
 -l  Load keys for encrypted filesystems as they are being mounted. This
     is equivalent to executing zfs load-key on each encryption root be‐
     fore mounting it. Note that if a filesystem has a keylocation of
     prompt this will cause the terminal to interactively block after
     asking for the key.
</code></pre></div>
</blockquote>
<p>Finally, create a dataset on <code>dpool</code> - it will also be encrypted:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ zfs create dpool/zfsdisks

$ zfs get encryption,keystatus dpool/zfsdisks
NAME            PROPERTY    VALUE        SOURCE
dpool/zfsdisks  encryption  aes-256-gcm  -
dpool/zfsdisks  keystatus   available    -
</code></pre></div>
<h2 id="set-up-mounting-aliases"><a href="#set-up-mounting-aliases" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Set up mounting aliases</h2>
<p>Let's set up aliases for the common used mounting commands in <code>/etc/profile.d/zfs-encryption.sh</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token builtin class-name">alias</span> mount-extbackup<span class="token operator">=</span><span class="token string">'zpool import dpool &#x26;&#x26; zfs load-key dpool &#x26;&#x26; zfs mount -l -a'</span>
<span class="token builtin class-name">alias</span> umount-extbackup<span class="token operator">=</span><span class="token string">'zfs unmount dpool &#x26;&#x26; zfs unload-key dpool &#x26;&#x26; zpool export dpool'</span>
</code></pre></div>
<p>We can now use those aliases to mount/unmount our external drive to <code>/dpool</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ mount-extbackup
$ <span class="token function">cat</span> /dpool/LABEL
$ umount-extbackup
</code></pre></div>
<h2 id="limit-ssh-access-to-pve-zsync-operations"><a href="#limit-ssh-access-to-pve-zsync-operations" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Limit SSH access to pve-zsync operations</h2>
<p>For security reasons, we want to <strong>limit SSH access on remote <code>backup</code> server</strong> only to the very specific commands which <code>pve-zsync</code> fires on remote side.</p>
<blockquote>
<p>In below <code>extbackup-zfs.sh</code> backup script, <code>pve-zsync</code> is executed as follows, e.g. for VMID 181:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ pve-zsync <span class="token function">sync</span> <span class="token parameter variable">--source</span> fatboy:dpool/zfsdisks/subvol-181-disk-1 <span class="token parameter variable">--dest</span> rpool/zfsdisks/subvol-181-disk-1 <span class="token parameter variable">--maxsnap</span> <span class="token number">12</span> <span class="token parameter variable">--name</span> extbackup
</code></pre></div>
<p>This produces the following remote commands...</p>
<p>Sample <code>pve-zsync</code> remote commands on first run:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ zfs list <span class="token parameter variable">-r</span> <span class="token parameter variable">-t</span> snapshot <span class="token parameter variable">-Ho</span> name <span class="token parameter variable">-S</span> creation dpool/zfsdisks/subvol-181-disk-1
$ zfs snapshot dpool/zfsdisks/subvol-181-disk-1@rep_extbackup_2021-07-12_23:15:53
$ zfs send -- dpool/zfsdisks/subvol-181-disk-1@rep_extbackup_2021-07-12_23:15:53
</code></pre></div>
<p>Sample <code>pve-zsnyc</code> remote commands on the following runs:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ zfs list <span class="token parameter variable">-rt</span> snapshot <span class="token parameter variable">-Ho</span> name dpool/zfsdisks/subvol-181-disk-1@rep_extbackup_2021-07-12_23:15:53
$ zfs send <span class="token parameter variable">-i</span> dpool/zfsdisks/subvol-181-disk-1@rep_extbackup_2021-07-12_23:15:53 -- dpool/zfsdisks/subvol-181-disk-1@rep_extbackup_2021-07-12_23:48:52
$ zfs destroy dpool/zfsdisks/subvol-181-disk-1@rep_extbackup_2021-07-12_23:13:13
</code></pre></div>
</blockquote>
<p>To accomplish this specific restrictions, we need to use a script in <code>command</code> section before the <code>extbackup</code>'s pubkey in <code>.ssh/authorized_keys</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token assign-left variable">command</span><span class="token operator">=</span><span class="token string">"/root/.ssh/allowed-commands-onlime.sh"</span>,no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-rsa AAAAB3N<span class="token punctuation">..</span>. root@extbackup
</code></pre></div>
<p><code>allowed-commands-onlime.sh</code> then does pattern matching to verify the commands - use <code>$SSH_ORIGINAL_COMMAND</code> to get the original command:</p>
<div class="nuxt-content-highlight"><span class="filename">allowed-commands-onlime.sh</span><pre class="line-numbers language-bash"><code><span class="token shebang important">#!/bin/bash</span>
<span class="token comment">#</span>
<span class="token comment"># https://serverfault.com/a/803873/299863</span>
<span class="token comment"># You can have only one forced command in ~/.ssh/authorized_keys. Use this wrapper to allow several commands.</span>
<span class="token comment"># And use https://3widgets.com/ - Regex Numeric Range Generator</span>
<span class="token comment">#</span>

<span class="token comment">## CONFIGURATION ##############################################</span>
<span class="token comment"># host range 194-254</span>
<span class="token assign-left variable">veid_pattern</span><span class="token operator">=</span><span class="token string">'(19[4-9]|2[0-4][0-9]|25[0-4])'</span>
<span class="token assign-left variable">backup_srcdir</span><span class="token operator">=</span><span class="token string">'/backup'</span>
<span class="token assign-left variable">pool_name</span><span class="token operator">=</span>dpool
<span class="token comment">###############################################################</span>

<span class="token comment"># Regex patterns</span>
<span class="token assign-left variable">ds_pattern</span><span class="token operator">=</span><span class="token string">"<span class="token variable">${pool_name}</span>/zfsdisks/subvol-<span class="token variable">${veid_pattern}</span>-disk-1"</span>
<span class="token assign-left variable">ts_pattern</span><span class="token operator">=</span><span class="token string">'20[2-9][0-9]-(0[1-9]|1[0-2])-(0[1-9]|[12][0-9]|3[01])_(0[0-9]|1[0-9]|2[0-3])(:(0[0-9]|[1-5][0-9])){2}'</span>
<span class="token assign-left variable">snap_prefix_pattern</span><span class="token operator">=</span><span class="token string">'rep_extbackup_([1-9]a_)?'</span>
<span class="token assign-left variable">snap_pattern</span><span class="token operator">=</span><span class="token string">"@<span class="token variable">${snap_prefix_pattern}</span><span class="token variable">${ts_pattern}</span>"</span>
<span class="token assign-left variable">ds_snap_pattern</span><span class="token operator">=</span><span class="token string">"<span class="token variable">${ds_pattern}</span><span class="token variable">${snap_pattern}</span>"</span>

<span class="token assign-left variable">patt1</span><span class="token operator">=</span><span class="token string">'zfs list -r( -)?t snapshot -Ho name( -S creation)? '</span><span class="token string">"<span class="token variable">$ds_pattern</span>(<span class="token variable">$snap_pattern</span>)?"</span>
<span class="token assign-left variable">patt2</span><span class="token operator">=</span><span class="token string">'zfs (snapshot|destroy) '</span><span class="token string">"<span class="token variable">$ds_snap_pattern</span>"</span>
<span class="token assign-left variable">patt3</span><span class="token operator">=</span><span class="token string">'zfs send '</span><span class="token string">"(-i <span class="token variable">$ds_snap_pattern</span> )?-- <span class="token variable">$ds_snap_pattern</span>"</span>
<span class="token assign-left variable">patt4</span><span class="token operator">=</span><span class="token string">"zfs rename <span class="token variable">$ds_snap_pattern</span> <span class="token variable">$ds_snap_pattern</span>"</span> <span class="token comment"># only used in extbackup-migrate-zfs-snaps.sh</span>

<span class="token assign-left variable">cmd</span><span class="token operator">=</span><span class="token string">"<span class="token variable">$SSH_ORIGINAL_COMMAND</span>"</span>

<span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token string">"<span class="token variable">$cmd</span>"</span> <span class="token operator">==</span> <span class="token string">'list-datasets'</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token comment"># special command to get a server list of all datasets and mountpoints</span>
    zfs list <span class="token parameter variable">-H</span> <span class="token parameter variable">-o</span> name,mountpoint,usedds <span class="token operator">|</span> <span class="token function">grep</span> <span class="token variable">$backup_srcdir</span>
    <span class="token builtin class-name">exit</span>
<span class="token keyword">elif</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">$cmd</span> <span class="token operator">=~</span> ^<span class="token variable">$patt1</span>$ <span class="token operator">||</span> <span class="token variable">$cmd</span> <span class="token operator">=~</span> ^<span class="token variable">$patt2</span>$ <span class="token operator">||</span> <span class="token variable">$cmd</span> <span class="token operator">=~</span> ^<span class="token variable">$patt3</span>$ <span class="token operator">||</span> <span class="token variable">$cmd</span> <span class="token operator">=~</span> ^<span class="token variable">$patt4</span>$ <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token variable">$SSH_ORIGINAL_COMMAND</span>
    <span class="token builtin class-name">exit</span>
<span class="token keyword">else</span>
    logger <span class="token string">"<span class="token variable"><span class="token variable">$(</span><span class="token function">basename</span> $0<span class="token variable">)</span></span> violation: <span class="token variable">$cmd</span>"</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"Access denied"</span>
    <span class="token builtin class-name">exit</span> <span class="token number">1</span>
<span class="token keyword">fi</span>
</code></pre></div>
<p>We can then test the magic <code>list-datasets</code> command to get a server list of all datasets and mountpoints:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>extbackup$ <span class="token function">ssh</span> backup list-datasets
</code></pre></div>
<p>This magic command is going to be used by our extbackup script to loop over ZFS datasets that need to be backupped.</p>
<h2 id="final-extbackup-script"><a href="#final-extbackup-script" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Final extbackup script</h2>
<p>The following script basically decrypts and mounts the external USB device, then runs <code>pve-zsync</code> to pull the whole data from <code>backup</code> for each dataset, and unmounts / closes the encrypted device right afterwards.</p>
<p><code>extbackup-zfs.sh</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">extbackup-zfs.sh</span><pre class="line-numbers language-bash"><code><span class="token shebang important">#!/bin/bash</span>

<span class="token comment">######### CONFIGURATION ############</span>
<span class="token assign-left variable">USB_DEVICE</span><span class="token operator">=</span>/dev/sdb
<span class="token assign-left variable">DISKS</span><span class="token operator">=</span><span class="token number">3</span>
<span class="token assign-left variable">DATAPOOL</span><span class="token operator">=</span>dpool
<span class="token assign-left variable">BKUP_SERVER</span><span class="token operator">=</span><span class="token string">"backup"</span>
<span class="token assign-left variable">BKUP_SRCDIR</span><span class="token operator">=</span><span class="token string">"/backup"</span>
<span class="token assign-left variable">MAXSNAP</span><span class="token operator">=</span><span class="token number">1</span>
<span class="token assign-left variable">KEYFILE</span><span class="token operator">=</span><span class="token string">"/mnt/ramfs/zfs_enc_key"</span>
<span class="token assign-left variable">LOGFILE</span><span class="token operator">=</span><span class="token string">"/var/log/extbackup.log"</span>
<span class="token assign-left variable">EXCLUDES</span><span class="token operator">=</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token comment"># datasets that should be excluded from backup</span>
<span class="token assign-left variable">SCRIPTNAME</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token function">basename</span> $0<span class="token variable">)</span></span>
<span class="token assign-left variable">CREATE_RERUN</span><span class="token operator">=</span><span class="token number">1</span> <span class="token comment"># default to creating rerun on failure</span>
<span class="token assign-left variable">RERUN_FILE</span><span class="token operator">=</span><span class="token string">"/tmp/<span class="token variable">$SCRIPTNAME</span>.rerun"</span>
<span class="token comment">####################################</span>

<span class="token function-name function">usage</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"Usage:"</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"<span class="token variable"><span class="token variable">$(</span><span class="token function">basename</span> $0<span class="token variable">)</span></span> [-m|--maxsnap N] [-r|--rerun]"</span>
    <span class="token builtin class-name">echo</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"Where:"</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"  -m|--maxsnap N  The number of snapshots to keep until older ones are erased (default: 1)"</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"  -r|--rerun      Rerun mode: rerun only if previous run has failed"</span>
    <span class="token builtin class-name">exit</span> <span class="token number">1</span>
<span class="token punctuation">}</span>

<span class="token comment"># Get options</span>
<span class="token keyword">while</span> <span class="token builtin class-name">:</span>
<span class="token keyword">do</span>
    <span class="token keyword">case</span> <span class="token variable">$1</span> <span class="token keyword">in</span>
        <span class="token parameter variable">-m</span> <span class="token operator">|</span> --maxsnap<span class="token punctuation">)</span>
            <span class="token assign-left variable">MAXSNAP</span><span class="token operator">=</span><span class="token string">"<span class="token variable">${2<span class="token operator">:-</span>$MAXSNAP}</span>"</span>
            <span class="token builtin class-name">shift</span>
            <span class="token builtin class-name">shift</span>
            <span class="token punctuation">;</span><span class="token punctuation">;</span>
        <span class="token parameter variable">-r</span> <span class="token operator">|</span> --rerun<span class="token punctuation">)</span>
            <span class="token assign-left variable">RERUN</span><span class="token operator">=</span><span class="token string">"true"</span>
            <span class="token builtin class-name">shift</span>
            <span class="token punctuation">;</span><span class="token punctuation">;</span>
        <span class="token parameter variable">-h</span> <span class="token operator">|</span> --help<span class="token punctuation">)</span>
            usage
            <span class="token punctuation">;</span><span class="token punctuation">;</span>
        *<span class="token punctuation">)</span>  <span class="token comment"># no more options. Stop while loop</span>
            <span class="token builtin class-name">break</span>
            <span class="token punctuation">;</span><span class="token punctuation">;</span>
    <span class="token keyword">esac</span>
<span class="token keyword">done</span>

<span class="token comment"># include common functions</span>
<span class="token builtin class-name">source</span> /usr/local/sbin/common-functions.sh
<span class="token comment"># ensure this is the only running instance</span>
assert_single_instance
<span class="token comment"># append STDOUT and STDERR to logfile and output both at the same time</span>
logall_output_stderr_stdout <span class="token variable">$LOGFILE</span>

<span class="token comment"># zfs command convienence aliases</span>
<span class="token assign-left variable">zfsgetval</span><span class="token operator">=</span><span class="token string">'zfs get -H -o value'</span>

<span class="token comment"># Rerun if `--rerun` was provided and previous run has failed</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token parameter variable">-n</span> <span class="token string">"<span class="token variable">$RERUN</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token operator">!</span> <span class="token parameter variable">-f</span> <span class="token variable">$RERUN_FILE</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
        <span class="token comment"># printinfo "did not find rerun file $RERUN_FILE; exiting OK"</span>
        <span class="token builtin class-name">exit</span>
    <span class="token keyword">fi</span>
<span class="token keyword">elif</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">$CREATE_RERUN</span> <span class="token parameter variable">-eq</span> <span class="token number">1</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token function">touch</span> <span class="token variable">$RERUN_FILE</span>
<span class="token keyword">fi</span>

<span class="token comment"># check if key file exists</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token operator">!</span> <span class="token parameter variable">-f</span> <span class="token variable">$KEYFILE</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    errquit <span class="token string">"The key file <span class="token variable">$KEYFILE</span> does not exist yet. Please run build-encryption-key.sh first!"</span>
<span class="token keyword">fi</span>

<span class="token comment"># First, make sure /dev/sdb will wake up and actually exists (using parted as a simple workaround)</span>
<span class="token function">parted</span> <span class="token parameter variable">-s</span> <span class="token variable">$USB_DEVICE</span> print <span class="token operator">></span> /dev/null
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$?</span> <span class="token parameter variable">-ne</span> <span class="token number">0</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    errquit <span class="token string">"External USB disk does not seem to show up as <span class="token variable">$USB_DEVICE</span>. Backup script aborted!"</span>
<span class="token keyword">fi</span>

<span class="token comment"># Import dpool, load encrytion key and mount all pools</span>
printinfo <span class="token string">"Importing ZFS pool <span class="token variable">$DATAPOOL</span> ..."</span>
zpool <span class="token function">import</span> <span class="token variable">$DATAPOOL</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$?</span> <span class="token parameter variable">-ne</span> <span class="token number">0</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    errquit <span class="token string">"Could not import <span class="token variable">$DATAPOOL</span>. Backup script aborted!"</span>
<span class="token keyword">fi</span>
printinfo <span class="token string">"Loading key for ZFS pool <span class="token variable">$DATAPOOL</span> ..."</span>
zfs load-key <span class="token variable">$DATAPOOL</span>
printinfo <span class="token string">"Mouting/decrypting all ZFS datasets ..."</span>
zfs <span class="token function">mount</span> <span class="token parameter variable">-l</span> <span class="token parameter variable">-a</span>

<span class="token comment"># Check if decryption/mounting of dpool was done</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$?</span> <span class="token parameter variable">-ne</span> <span class="token number">0</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    errquit <span class="token string">"Could not mount/decrypt ZFS filesystems. Backup script aborted!"</span>
<span class="token keyword">fi</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token string">"<span class="token variable"><span class="token variable">$(</span>$zfsgetval keystatus $DATAPOOL<span class="token variable">)</span></span>"</span> <span class="token operator">!=</span> <span class="token string">"available"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    errquit <span class="token string">"ZFS dataset <span class="token variable">$DATAPOOL</span> does not seem to be correctly decrypted. Backup script aborted!"</span>
<span class="token keyword">fi</span>

<span class="token comment"># abort on missing /dpool/LABEL or wrongly formatted label</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token operator">!</span> <span class="token parameter variable">-f</span> /<span class="token variable">$DATAPOOL</span>/LABEL <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    errquit <span class="token string">"Disk label (/<span class="token variable">$DATAPOOL</span>/LABEL) does not exist. Backup script aborted!!!"</span>
<span class="token keyword">fi</span>
<span class="token assign-left variable">label</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token function">cat</span> /$DATAPOOL/LABEL<span class="token variable">)</span></span>
<span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token operator">!</span> <span class="token variable">$label</span> <span class="token operator">=~</span> ^<span class="token punctuation">[</span><span class="token number">1</span>-<span class="token variable">$DISKS</span><span class="token punctuation">]</span>a$ <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    errquit <span class="token string">"Invalid label in /<span class="token variable">$DATAPOOL</span>/LABEL: <span class="token variable">$label</span>"</span>
<span class="token keyword">fi</span>

printinfo <span class="token string">"=== STARTING EXTBACKUP TO DISK WITH LABEL <span class="token variable">$label</span> ==="</span>

<span class="token comment"># loop over all datasets and run pve-zsync</span>
<span class="token comment"># `list-datasets` is a magic command that is resolved to `zfs list -H -o name,mountpoint,usedds | grep /backup` on remote</span>
<span class="token function">ssh</span> <span class="token variable">$BKUP_SERVER</span> list-datasets <span class="token operator">|</span> <span class="token keyword">while</span> <span class="token builtin class-name">read</span> dataset mountpoint usedds<span class="token punctuation">;</span> <span class="token keyword">do</span>
    <span class="token assign-left variable">zsync</span><span class="token operator">=</span><span class="token string">"pve-zsync sync --source <span class="token variable">$BKUP_SERVER</span>:<span class="token variable">$dataset</span> --dest <span class="token variable"><span class="token variable">$(</span><span class="token function">dirname</span> $dataset<span class="token variable">)</span></span> --maxsnap <span class="token variable">$MAXSNAP</span> --name extbackup_<span class="token variable">$label</span>"</span>
    <span class="token comment"># check if the dataset should be excluded from extbackup</span>
    <span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">${EXCLUDES<span class="token punctuation">[</span>*<span class="token punctuation">]</span>}</span> <span class="token operator">=~</span> <span class="token variable">$dataset</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
        printwarn <span class="token string">"(SKIPPED) <span class="token variable">$zsync</span>"</span>
        <span class="token builtin class-name">continue</span>
    <span class="token keyword">fi</span>

    <span class="token comment"># check if dataset exists</span>
    <span class="token keyword">if</span> zfs list <span class="token variable">$dataset</span> <span class="token operator">&#x26;></span>/dev/null<span class="token punctuation">;</span> <span class="token keyword">then</span>
        <span class="token comment"># unmount dataset prior to pve-zsync run to avoid "dataset is busy"</span>
        zfs unmount <span class="token variable">$dataset</span>
    <span class="token keyword">fi</span>
    printinfo <span class="token string">"<span class="token variable">$zsync</span> # <span class="token variable">$usedds</span>"</span>
    <span class="token comment"># use `&#x3C; /dev/null` as workaround to avoid pve-zsync breaking out of while loop (as it somehow expects input)</span>
    <span class="token variable">$zsync</span> <span class="token operator">&#x3C;</span> /dev/null

    <span class="token comment"># set mountpoint to the same as on remote backup server</span>
    <span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token string">"<span class="token variable"><span class="token variable">$(</span>$zfsgetval mountpoint $dataset<span class="token variable">)</span></span>"</span> <span class="token operator">!=</span> <span class="token string">"<span class="token variable">$mountpoint</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
        <span class="token assign-left variable">cmd</span><span class="token operator">=</span><span class="token string">"zfs set mountpoint=<span class="token variable">$mountpoint</span> <span class="token variable">$dataset</span>"</span>
        printinfo <span class="token string">"<span class="token variable">$cmd</span>"</span>
        <span class="token variable">$cmd</span>
        <span class="token function">sleep</span> <span class="token number">1</span>
    <span class="token keyword">else</span>
        zfs <span class="token function">mount</span> <span class="token variable">$dataset</span>
    <span class="token keyword">fi</span>
<span class="token keyword">done</span>

printinfo <span class="token string">"Unmounting/encrypting ZFS pool <span class="token variable">$DATAPOOL</span> ..."</span>
zfs unmount <span class="token variable">$DATAPOOL</span>
printinfo <span class="token string">"Unloading key for ZFS pool <span class="token variable">$DATAPOOL</span> ..."</span>
zfs unload-key <span class="token variable">$DATAPOOL</span>
printinfo <span class="token string">"Exporting ZFS pool <span class="token variable">$DATAPOOL</span> ..."</span>
zpool <span class="token builtin class-name">export</span> <span class="token variable">$DATAPOOL</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$?</span> <span class="token parameter variable">-ne</span> <span class="token number">0</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    errquit <span class="token string">"Could not export <span class="token variable">$DATAPOOL</span>. Please fix!"</span>
<span class="token keyword">fi</span>

<span class="token comment"># Remove rerun file on successful run</span>
<span class="token function">rm</span> <span class="token parameter variable">-f</span> <span class="token variable">$RERUN_FILE</span>

<span class="token function">sync</span>
printinfo <span class="token string">"DONE."</span>
</code></pre></div>
<p><code>common-functions.sh</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">common-functions.sh</span><pre class="line-numbers language-bash"><code><span class="token shebang important">#!/bin/bash</span>

<span class="token comment">####### CONFIGURATION #################</span>
<span class="token assign-left variable">TSFORMAT</span><span class="token operator">=</span><span class="token string">"%Y-%m-%d %H:%M:%S"</span>
<span class="token assign-left variable">EXECUTING_SCRIPT</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token function">basename</span> $0<span class="token variable">)</span></span>
<span class="token comment">#######################################</span>

<span class="token function-name function">printinfo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"[<span class="token variable"><span class="token variable">`</span><span class="token function">date</span> +<span class="token string">"<span class="token variable">$TSFORMAT</span>"</span><span class="token variable">`</span></span>] <span class="token variable">$1</span>"</span>
<span class="token punctuation">}</span>

<span class="token function-name function">printwarn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"[<span class="token variable"><span class="token variable">`</span><span class="token function">date</span> +<span class="token string">"<span class="token variable">$TSFORMAT</span>"</span><span class="token variable">`</span></span>] WARNING: <span class="token variable">$1</span>"</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">--color</span> <span class="token string">"WARNING"</span>
<span class="token punctuation">}</span>

<span class="token function-name function">printerr</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token operator">></span><span class="token file-descriptor important">&#x26;2</span> <span class="token builtin class-name">echo</span> <span class="token string">"[<span class="token variable"><span class="token variable">`</span><span class="token function">date</span> +<span class="token string">"<span class="token variable">$TSFORMAT</span>"</span><span class="token variable">`</span></span>] ERROR: <span class="token variable">$1</span>"</span>
<span class="token punctuation">}</span>

<span class="token function-name function">errquit</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token parameter variable">-n</span> <span class="token string">"<span class="token variable">$1</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
        printerr <span class="token string">"<span class="token variable">$1</span>"</span>
    <span class="token keyword">fi</span>
    <span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token parameter variable">-n</span> <span class="token string">"<span class="token variable">$LOCKFILE</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
        <span class="token function">rm</span> <span class="token parameter variable">-f</span> <span class="token variable">$LOCKFILE</span>
    <span class="token keyword">fi</span>
    <span class="token builtin class-name">exit</span> <span class="token number">1</span>
<span class="token punctuation">}</span>

<span class="token function-name function">assert_single_instance</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token comment"># Check if another instance of this script is already running</span>
    <span class="token comment"># https://stackoverflow.com/a/16807995/5982842</span>
    <span class="token keyword">for</span> <span class="token for-or-select variable">pid</span> <span class="token keyword">in</span> <span class="token variable"><span class="token variable">$(</span>pidof <span class="token parameter variable">-x</span> $EXECUTING_SCRIPT<span class="token variable">)</span></span><span class="token punctuation">;</span> <span class="token keyword">do</span>
        <span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$pid</span> <span class="token operator">!=</span> <span class="token variable">$$</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
            errquit <span class="token string">"<span class="token variable">$EXECUTING_SCRIPT</span> process is already running with PID <span class="token variable">$pid</span>"</span>
        <span class="token keyword">fi</span>
    <span class="token keyword">done</span>
<span class="token punctuation">}</span>

<span class="token function-name function">logall_output_stderr_stdout</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token assign-left variable">logfile</span><span class="token operator">=</span><span class="token variable">${1<span class="token operator">:-</span>${EXECUTING_SCRIPT}</span>.log<span class="token punctuation">}</span>
    <span class="token comment"># Append STDOUT and STDERR to logfile and output both at the same time</span>
    <span class="token builtin class-name">exec</span> <span class="token operator">></span>  <span class="token operator">></span><span class="token punctuation">(</span><span class="token function">tee</span> <span class="token parameter variable">-ia</span> <span class="token variable">$logfile</span><span class="token punctuation">)</span>
    <span class="token builtin class-name">exec</span> <span class="token operator"><span class="token file-descriptor important">2</span>></span> <span class="token operator">></span><span class="token punctuation">(</span><span class="token function">tee</span> <span class="token parameter variable">-ia</span> <span class="token variable">$logfile</span> <span class="token operator">></span><span class="token file-descriptor important">&#x26;2</span><span class="token punctuation">)</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Run this as a weekly cronjob, e.g. via <code>/etc/cron.d/extbackup</code>, starting Sat morning:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token comment"># weekly extbackup on encrypted USB drive (Sat 07:30AM), rerun on failure the next 3 days</span>
<span class="token number">30</span> 07   * * <span class="token number">6</span>   root    extbackup-zfs.sh
<span class="token number">30</span> 07   * * <span class="token number">0</span>-2 root    extbackup-zfs.sh <span class="token parameter variable">--rerun</span>    
</code></pre></div>
<p>On first run (e.g. in a screen session instead of via cronjob), make sure you follow the log:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">tail</span> <span class="token parameter variable">-f</span> /var/log/extbackup.log
</code></pre></div>
<h2 id="set-up-another-encrypted-external-drive"><a href="#set-up-another-encrypted-external-drive" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Set up another encrypted external drive</h2>
<p>We want to rotate several external drives, storing them in different physical locations. Setting up an additional drive should not require us to transfer all data again from <code>backup</code> server (which in our case was > 4TB of data that needed to get transferred between two datacenters). We want to start from the latest external drive and replicate its data instead and then only continue with incremental backups.</p>
<h3 id="idea"><a href="#idea" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Idea</h3>
<p><strong>Problem:</strong> ZFS does not support duplicating/copying of existing snapshots on the same dataset (with a new snapshot name, obviously). So, the initial idea was to set up encryption on new disk first, then connect first disk and import secondary as <code>dpool2</code>, then copying all datasets over using <code>zfs send|receive</code>. That would force us to write another complex script. Way to complicated! Let's use <strong>mirroring to replicate the full disk</strong> instead!</p>
<p><strong>Solution:</strong></p>
<ul>
<li><strong>Part 1) Replicate Disk</strong>
<ol>
<li>Connect existing disk with LABEL <code>1a</code> and import dpool: <code>zpool import dpool</code></li>
<li>Connect <strong>new (empty) disk and attach it to <code>dpool</code></strong>, creating a mirror: <code>zpool attach dpool sdb sdd</code></li>
<li>Wait until <strong>resilvering</strong> has completed</li>
<li><strong>Split mirror</strong> again to have new disk in a separate <code>dpool2</code>: <code>zpool split dpool dpool2</code></li>
<li>Export first <code>dpool</code>, disconnect first disk, and <strong>import <code>dpool2</code> by renaming it to <code>dpool</code></strong>: <code>zpool import dpool2 dpool</code></li>
<li>Export <code>dpool</code> (now the new final replicated disk), never forget exporting before detaching a disk!</li>
</ol>
</li>
<li><strong>Part 2) Renaming LABEL / snapshots</strong>
<ol>
<li>Rember, we have a full copy of initial disk with LABEL <code>1a</code> on new disk which should be renamed to <code>2a</code></li>
<li>mount/decrypt new disk and <strong>check label</strong> (still must be the same as the previous disk!), unmount/encrypt</li>
<li>run <code>extbackup-zfs.sh --maxsnap 2</code>  with old LABEL, to <strong>create new snapshots</strong> that can be renamed (and keeping first ones alive on <code>backup</code> for first disk)</li>
<li><strong>change LABEL</strong></li>
<li>run <code>extbackup-migrate-zfs-snaps.sh</code> to <strong>rename all local and remote (<code>backup</code>) snapshots</strong> to new LABEL</li>
<li><strong>purge local snapshots</strong> with old label by running <code>extbackup-migrate-zfs-snaps.sh --purge</code></li>
<li>unmount/encrypt disk</li>
</ol>
</li>
</ul>
<h3 id="part-1-replicate-a-disk"><a href="#part-1-replicate-a-disk" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Part 1) Replicate a disk</h3>
<p>Replicate a full disk by mirorring <code>dpool</code> :</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token comment"># On extbackup with 1st disk (LABEL 1a) connected</span>
$ zpool <span class="token function">import</span> dpool
$ zpool status dpool
  pool: dpool
 state: ONLINE
config:
    NAME        STATE     READ WRITE CKSUM
    dpool       ONLINE       <span class="token number">0</span>     <span class="token number">0</span>     <span class="token number">0</span>
      sdb       ONLINE       <span class="token number">0</span>     <span class="token number">0</span>     <span class="token number">0</span>

<span class="token comment"># Connect 2nd disk (already partitioned or not should not matter)</span>
$ lsblk
NAME   MAJ:MIN RM   SIZE RO TYPE MOUNTPOINT
sdb      <span class="token number">8</span>:16   <span class="token number">0</span>   <span class="token number">7</span>.3T  <span class="token number">0</span> disk 
├─sdb1   <span class="token number">8</span>:17   <span class="token number">0</span>   <span class="token number">7</span>.3T  <span class="token number">0</span> part 
└─sdb9   <span class="token number">8</span>:25   <span class="token number">0</span>    64M  <span class="token number">0</span> part 
sdd      <span class="token number">8</span>:48   <span class="token number">0</span>   <span class="token number">7</span>.3T  <span class="token number">0</span> disk 

<span class="token comment"># Attach new device to existing zpool, creating a mirror</span>
$ zpool attach dpool sdb sdd
<span class="token comment"># (maybe) need to use --force if the disk was already initialized before</span>
$ zpool attach dpool sdb sdd <span class="token parameter variable">-f</span>
<span class="token comment"># Resilvering starts immediately.</span>

<span class="token comment"># Wait until fully resilvered ...</span>
$ zpool status dpool
  pool: dpool
 state: ONLINE
  scan: resilvered <span class="token number">4</span>.73T <span class="token keyword">in</span> 09:15:53 with <span class="token number">0</span> errors on <span class="token punctuation">..</span>.
config:
    NAME        STATE     READ WRITE CKSUM
    dpool       ONLINE       <span class="token number">0</span>     <span class="token number">0</span>     <span class="token number">0</span>
      mirror-0  ONLINE       <span class="token number">0</span>     <span class="token number">0</span>     <span class="token number">0</span>
        sdb     ONLINE       <span class="token number">0</span>     <span class="token number">0</span>     <span class="token number">0</span>
        sdd     ONLINE       <span class="token number">0</span>     <span class="token number">0</span>     <span class="token number">0</span>
</code></pre></div>
<p>You can use the <code>zpool split</code> command to detach disks from a mirrored ZFS storage pool to create a new pool with one of the detached disks. The new pool will have identical contents to the original mirrored ZFS storage pool:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ zpool <span class="token function">split</span> dpool dpool2
<span class="token comment"># dpool2 is not imported automatically, which is what we want!</span>

<span class="token comment"># now, export dpool to free this pools name</span>
$ zpool <span class="token builtin class-name">export</span> dpool

<span class="token comment"># rename new dpool2 to dpool and export it again</span>
$ zpool <span class="token function">import</span> dpool2 dpool
$ zpool <span class="token builtin class-name">export</span> dpool

<span class="token comment"># disconnect both disks and list importable pools after reconnecting the second disk (should then show up as /dev/sdb)</span>
$ zpool <span class="token function">import</span>
   pool: dpool
     id: <span class="token number">9764679279839179262</span>
  state: ONLINE
 action: The pool can be imported using its name or numeric identifier.
 config:
    dpool                                          ONLINE
      ata-Samsung_SSD_870_QVO_8TB_S5SSNF0R201266B  ONLINE

<span class="token comment"># import it to check status</span>
$ zpool <span class="token function">import</span> dpool
$ zpool status dpool
  pool: dpool
 state: ONLINE
  scan: resilvered <span class="token number">4</span>.61T <span class="token keyword">in</span> 08:45:42 with <span class="token number">0</span> errors on <span class="token punctuation">..</span>.
config:
    NAME                                           STATE     READ WRITE CKSUM
    dpool                                          ONLINE       <span class="token number">0</span>     <span class="token number">0</span>     <span class="token number">0</span>
      ata-Samsung_SSD_870_QVO_8TB_S5SSNF0R201266B  ONLINE       <span class="token number">0</span>     <span class="token number">0</span>     <span class="token number">0</span>

$ zpool <span class="token builtin class-name">export</span> dpool
</code></pre></div>
<p>All cool! We now have two identical disks, both still having label set to <code>1a</code> in <code>/dpool/LABEL</code>.</p>
<h3 id="part-2-renaming-label--snapshots"><a href="#part-2-renaming-label--snapshots" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Part 2) Renaming LABEL / snapshots</h3>
<p>Now, set up new replicated disk:</p>
<ol>
<li>mount/decrypt new disk and <strong>check label</strong> (still must be the same as the previous disk!), unmount/encrypt</li>
<li>run <code>extbackup-zfs.sh --maxsnap 2</code>  with old LABEL, to <strong>create new snapshots</strong> that can be renamed (and keeping first ones alive on <code>backup</code> for first disk).</li>
<li><strong>change LABEL</strong> to <code>2a</code>, <code>3a</code>,...</li>
<li>run <code>extbackup-migrate-zfs-snaps.sh</code> to <strong>rename all local and remote (<code>backup</code>) snapshots</strong> to new LABEL</li>
<li><strong>purge local snapshots</strong> with old label by running <code>extbackup-migrate-zfs-snaps.sh --purge</code></li>
<li>unmount/encrypt disk</li>
</ol>
<blockquote>
<p>For mounting/dencrytion and encryption/unmounting, we're using the following aliases in below commands:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token builtin class-name">alias</span> mount-extbackup<span class="token operator">=</span><span class="token string">'zpool import dpool &#x26;&#x26; zfs load-key dpool &#x26;&#x26; zfs mount -l -a'</span>
<span class="token builtin class-name">alias</span> umount-extbackup<span class="token operator">=</span><span class="token string">'zfs unmount dpool &#x26;&#x26; zfs unload-key dpool &#x26;&#x26; zpool export dpool'</span>
</code></pre></div>
</blockquote>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ mount-extbackup
<span class="token comment"># check LABEL (must match previous disk, DON'T CHANGE IT YET!) and review snapshots</span>
$ <span class="token function">cat</span> /dpool/LABEL
1a
$ zfs list <span class="token parameter variable">-H</span> <span class="token parameter variable">-t</span> snapshot <span class="token parameter variable">-o</span> name

<span class="token comment"># ... and unmount it again (to be ready for extbackup-zfs.sh)</span>
$ umount-extbackup

<span class="token comment"># run extbackup by keeping 2 snapshots (and still pretending we're 1a, keeping old LABEL !!)</span>
$ <span class="token function">screen</span>
$ extbackup-zfs.sh <span class="token parameter variable">--maxsnap</span> <span class="token number">2</span>

<span class="token comment"># after extbackup completion change LABEL</span>
$ mount-extbackup
$ <span class="token builtin class-name">echo</span> <span class="token parameter variable">-n</span> <span class="token string">"2a"</span> <span class="token operator">></span> /dpool/LABEL

<span class="token comment"># Rename latest @rep_extbackup_1a_* snapshots to @rep_extbackup_2a_* (new LABEL)</span>
$ extbackup-migrate-zfs-snaps.sh <span class="token parameter variable">--dryrun</span>
<span class="token comment"># This will rename them both locally and on remote backup server</span>
$ extbackup-migrate-zfs-snaps.sh

<span class="token comment"># destroy old snapshots (only on extbackup, KEEP THEM ON backup !!!)</span>
$ extbackup-migrate-zfs-snaps.sh <span class="token parameter variable">--purge</span> <span class="token parameter variable">--dryrun</span>
$ extbackup-migrate-zfs-snaps.sh <span class="token parameter variable">--purge</span>
<span class="token comment"># review (there should now only be @rep_extbackup_2a_* snapshots)</span>
$ zfs list <span class="token parameter variable">-H</span> <span class="token parameter variable">-t</span> snapshot <span class="token parameter variable">-o</span> name <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">-v</span> @rep_extbackup_2a

<span class="token comment"># unmount/encrypt disk</span>
$ umount-extbackup
</code></pre></div>
<p>The disk can now be unplugged and is ready for the next backup run.</p>
<p>Helper script <code>extbackup-migrate-zfs-snaps.sh</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">extbackup-migrate-zfs-snaps.sh</span><pre class="line-numbers language-bash"><code><span class="token shebang important">#!/bin/bash</span>
<span class="token comment">######### CONFIGURATION ############</span>
<span class="token assign-left variable">DISKS</span><span class="token operator">=</span><span class="token number">3</span>
<span class="token assign-left variable">DATAPOOL</span><span class="token operator">=</span>dpool
<span class="token assign-left variable">OLDLABEL</span><span class="token operator">=</span>1a
<span class="token assign-left variable">BKUP_SERVER</span><span class="token operator">=</span><span class="token string">"backup"</span>
<span class="token comment">####################################</span>

<span class="token function-name function">usage</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"Usage:"</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"<span class="token variable"><span class="token variable">$(</span><span class="token function">basename</span> $0<span class="token variable">)</span></span> [-l|--label OLDLABEL]"</span>
    <span class="token builtin class-name">echo</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"Where:"</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"  -l|--label OLDLABEL  The label of the disks where the previous snapshots have been performed (default: 1a)"</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"  -s|--dryrun          Dry-run / simulation mode: Don't rename any snapshots"</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"  -p|-purge            Purge all snapshots that don't match current disk label (only run this as a final migration!)"</span>
    <span class="token builtin class-name">exit</span> <span class="token number">1</span>
<span class="token punctuation">}</span>

<span class="token comment"># Get options</span>
<span class="token keyword">while</span> <span class="token builtin class-name">:</span>
<span class="token keyword">do</span>
    <span class="token keyword">case</span> <span class="token variable">$1</span> <span class="token keyword">in</span>
        <span class="token parameter variable">-l</span> <span class="token operator">|</span> --label<span class="token punctuation">)</span>
            <span class="token assign-left variable">OLDLABEL</span><span class="token operator">=</span><span class="token string">"<span class="token variable">${2<span class="token operator">:-</span>$OLDLABEL}</span>"</span>
            <span class="token builtin class-name">shift</span>
            <span class="token builtin class-name">shift</span>
            <span class="token punctuation">;</span><span class="token punctuation">;</span>
        <span class="token parameter variable">-s</span> <span class="token operator">|</span> --dryrun<span class="token punctuation">)</span>
            <span class="token assign-left variable">DRYRUN</span><span class="token operator">=</span><span class="token string">"true"</span>
            <span class="token builtin class-name">shift</span>
            <span class="token punctuation">;</span><span class="token punctuation">;</span>
        <span class="token parameter variable">-p</span> <span class="token operator">|</span> --purge<span class="token punctuation">)</span>
            <span class="token assign-left variable">PURGE</span><span class="token operator">=</span><span class="token string">"true"</span>
            <span class="token builtin class-name">shift</span>
            <span class="token punctuation">;</span><span class="token punctuation">;</span>
        <span class="token parameter variable">-h</span> <span class="token operator">|</span> --help<span class="token punctuation">)</span>
            usage
            <span class="token punctuation">;</span><span class="token punctuation">;</span>
        *<span class="token punctuation">)</span>  <span class="token comment"># no more options. Stop while loop</span>
            <span class="token builtin class-name">break</span>
            <span class="token punctuation">;</span><span class="token punctuation">;</span>
    <span class="token keyword">esac</span>
<span class="token keyword">done</span>

<span class="token comment"># include common functions</span>
<span class="token builtin class-name">source</span> /usr/local/sbin/common-functions.sh

<span class="token comment"># zfs command convienence aliases</span>
<span class="token assign-left variable">zfslistname</span><span class="token operator">=</span><span class="token string">'zfs list -H -o name'</span>

<span class="token comment"># abort on missing label</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token operator">!</span> <span class="token parameter variable">-f</span> /<span class="token variable">$DATAPOOL</span>/LABEL <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    errquit <span class="token string">"Disk label (/<span class="token variable">$DATAPOOL</span>/LABEL) does not exist. Backup script aborted!!!"</span>
<span class="token keyword">fi</span>
<span class="token assign-left variable">label</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span><span class="token function">cat</span> /$DATAPOOL/LABEL<span class="token variable">)</span></span>
<span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token operator">!</span> <span class="token variable">$label</span> <span class="token operator">=~</span> ^<span class="token punctuation">[</span><span class="token number">1</span>-<span class="token variable">$DISKS</span><span class="token punctuation">]</span>a$ <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    errquit <span class="token string">"Invalid label in /<span class="token variable">$DATAPOOL</span>/LABEL: <span class="token variable">$label</span>"</span>
<span class="token keyword">fi</span>

<span class="token assign-left variable">dspattern</span><span class="token operator">=</span><span class="token string">"<span class="token variable">$DATAPOOL</span>/zfsdisks/[[:alnum:]-]+"</span>
<span class="token keyword">for</span> <span class="token for-or-select variable">dataset</span> <span class="token keyword">in</span> <span class="token variable"><span class="token variable">$(</span>$zfslistname <span class="token parameter variable">-t</span> filesystem <span class="token parameter variable">-d</span> <span class="token number">2</span> $DATAPOOL<span class="token variable">)</span></span><span class="token punctuation">;</span> <span class="token keyword">do</span>
    <span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable">$dataset</span> <span class="token operator">=~</span> ^<span class="token variable">$dspattern</span>$ <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
        <span class="token comment"># local purge run (as a final migration)</span>
        <span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token parameter variable">-n</span> <span class="token string">"<span class="token variable">$PURGE</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
            <span class="token keyword">for</span> <span class="token for-or-select variable">snapshot</span> <span class="token keyword">in</span> <span class="token variable"><span class="token variable">$(</span>$zfslistname <span class="token parameter variable">-t</span> snapshot <span class="token parameter variable">-s</span> creation $dataset <span class="token operator">|</span> <span class="token function">grep</span> @rep_extbackup_$<span class="token punctuation">{</span>OLDLABEL<span class="token punctuation">}</span>_<span class="token variable">)</span></span><span class="token punctuation">;</span> <span class="token keyword">do</span>
                <span class="token assign-left variable">cmd</span><span class="token operator">=</span><span class="token string">"zfs destroy <span class="token variable">$snapshot</span>"</span>
                <span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token parameter variable">-z</span> <span class="token string">"<span class="token variable">$DRYRUN</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
                    printinfo <span class="token string">"<span class="token variable">$cmd</span>"</span>
                    <span class="token variable">$cmd</span>
                <span class="token keyword">else</span>
                    printinfo <span class="token string">"(DRYRUN) <span class="token variable">$cmd</span>"</span>
                <span class="token keyword">fi</span>
            <span class="token keyword">done</span>
            <span class="token builtin class-name">continue</span> <span class="token comment"># don't proceed with snapshot renaming below, as that is part of first migration</span>
        <span class="token keyword">fi</span>

        <span class="token comment"># snapshot renaming (first migration)</span>
        <span class="token assign-left variable">snapold</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span>$zfslistname <span class="token parameter variable">-t</span> snapshot <span class="token parameter variable">-s</span> creation $dataset <span class="token operator">|</span> <span class="token function">grep</span> @rep_extbackup <span class="token operator">|</span> <span class="token function">tail</span> <span class="token parameter variable">-n1</span><span class="token variable">)</span></span>
        <span class="token keyword">if</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token parameter variable">-z</span> <span class="token string">"<span class="token variable">$snapold</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
            printwarn <span class="token string">"Skipping <span class="token variable">$dataset</span>: no extbackup snapshot found."</span>
            <span class="token builtin class-name">continue</span>
        <span class="token keyword">fi</span>
        <span class="token assign-left variable">snapnew</span><span class="token operator">=</span><span class="token string">"<span class="token variable">${snapold<span class="token operator">/</span>@rep_extbackup_$OLDLABEL<span class="token operator">/</span>@rep_extbackup_$label}</span>"</span>
        <span class="token assign-left variable">cmd</span><span class="token operator">=</span><span class="token string">"zfs rename <span class="token variable">$snapold</span> <span class="token variable">$snapnew</span>"</span>
        <span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token parameter variable">-z</span> <span class="token string">"<span class="token variable">$DRYRUN</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
            printinfo <span class="token string">"<span class="token variable">$cmd</span>"</span>
            <span class="token variable">$cmd</span>
            <span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$?</span> <span class="token parameter variable">-ne</span> <span class="token number">0</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
                errquit <span class="token string">"Failed to locally rename snapshot. Migration script aborted!"</span>
            <span class="token keyword">fi</span>
            <span class="token function">ssh</span> <span class="token variable">$BKUP_SERVER</span> <span class="token variable">$cmd</span>
            <span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token variable">$?</span> <span class="token parameter variable">-ne</span> <span class="token number">0</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
                errquit <span class="token string">"Failed to remotely rename snapshot on <span class="token variable">$BKUP_SERVER</span>. Migration script aborted!"</span>
            <span class="token keyword">fi</span>
        <span class="token keyword">else</span>
            printinfo <span class="token string">"(DRYRUN) <span class="token variable">$cmd</span>"</span>
        <span class="token keyword">fi</span>
    <span class="token keyword">fi</span>
<span class="token keyword">done</span>
</code></pre></div>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="proxmoxve" term="proxmoxve"/>
        <category label="backup" term="backup"/>
        <category label="security" term="security"/>
        <category label="zfs" term="zfs"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[APFS Encryption Status]]></title>
        <id>https://pipo.blog/articles/20210728-apfs-encryption-status</id>
        <link href="https://pipo.blog/articles/20210728-apfs-encryption-status"/>
        <updated>2021-07-28T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Show APFS encryption status of an external drive on macOS.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/apfs-apple-file-system.jpg"></p>
                      <p>macOS offers us a super simple way to fully encrypt an APFS formatted external drive: Right-click on the volume in the Finder and choose the option "Encrypt". Carbon Copy Cloner (CCC) also recommends this in its KB article <a href="https://bombich.com/kb/ccc6/working-filevault-encryption" rel="nofollow noopener noreferrer" target="_blank">Enabling encryption on a volume that will not contain an installation of macOS</a>.</p>
<p>But macOS is a bit too transparent on that task. It simply starts encrypting the volume without any feedback or progress bar (WTF?). Even Disk Utility just shows the volume as <code>APFS (Encrypted)</code> - as it would have already completed full volume encryption.</p>
<!--more-->
<p>The <code>fdesetup</code> utility (FileVault configuration tool) gives us more details, e.g.:</p>
<div class="nuxt-content-highlight"><pre class="language-bash line-numbers"><code>$ fdesetup status <span class="token parameter variable">-device</span> /Volumes/MY_DRIVE <span class="token parameter variable">-extend</span>
FileVault is On.
Volume is APFS. <span class="token punctuation">(</span>FileVault Enabled<span class="token punctuation">)</span>
Encryption <span class="token keyword">in</span> progress: Percent completed <span class="token operator">=</span> <span class="token number">11</span>. Estimated completion <span class="token keyword">in</span> <span class="token punctuation">(</span>Calculating<span class="token punctuation">)</span>
Encryption <span class="token keyword">in</span> progress: Percent completed <span class="token operator">=</span> <span class="token number">47</span>. Estimated completion <span class="token keyword">in</span> <span class="token number">5</span> hours <span class="token number">5</span> minutes
</code></pre></div>
<p>In case you won't specify <code>-device</code>, it will just display the FileVault encryption status of your system volume. On APFS volumes, the <code>-extended</code> option will give continuous updates and estimated completion times during encryption and decryption phases.</p>
<p>Exactly what I was looking for!</p>
<p>... and in case you have plugged the device into the wrong MacBook, the one with which you need to run out of house and hop on your bike right now: No worries, simply unmount the external drive. Full disk encryption will continue next time you plug it in again.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="macos" term="macos"/>
        <category label="security" term="security"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[MySQL FEDERATED Storage Engine and Replication]]></title>
        <id>https://pipo.blog/articles/20210724-mysql-federated</id>
        <link href="https://pipo.blog/articles/20210724-mysql-federated"/>
        <updated>2021-07-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Use MySQL FEDERATED storage engine to store data on a remote MySQL server that acts as a replication master.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/mysql-federated.png"></p>
                      <p>Back in Nov 2020, it got time to rethink the MySQL replication infrastructure for <a href="https://my.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Airpane Controlpanel</a>, our customer dashboard at <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime GmbH</a>. The whole application runs on a separate server, but an extract of mail account information (mail account credentials, mail mappings/forwardings) needs to get replicated to our 3 mail servers (<code>mailsrv</code>, <code>mx1</code>, <code>mx2</code>). At that time, I was using MySQL MASTER-SLAVE replication for a single database in a 4-node setup (1 master + 3 slaves).</p>
<p>For security reasons, I no longer wanted any mail server to have access to the binlog of the server that hosted our controlpanel (even though I already had that limited to a single database with mailserver related data only). I also wanted to reduce complexity a bit, just using MySQL replication for the 3 mail servers and propagating <code>mailsrv</code> to master.</p>
<p>The mailserver data is extracted from our controlpanel database by MySQL triggers (mainly <code>AFTER INSERT</code>, <code>AFTER UPDATE</code>, and <code>BEFORE DELETE</code> triggers) into a separate database <code>mailsync</code>. How to get whole <code>mailsync</code> data stored on the remote server <code>mailsrv</code> without using MySQL replication? I didn't want to care about this on application level.</p>
<p>That's where <a href="https://dev.mysql.com/doc/refman/8.0/en/federated-storage-engine.html" rel="nofollow noopener noreferrer" target="_blank">MySQL FEDERATED Storage Engine</a> comes into play!</p>
<!--more-->
<p>MySQL reference manual gives us a nice explanation for <code>FEDERATED</code> storage engine:</p>
<blockquote>
<p>The <code>FEDERATED</code> storage engine lets you access data from a remote MySQL database without using replication or cluster technology. Querying a local <code>FEDERATED</code> table automatically pulls the data from the remote (federated) tables. No data is stored on the local tables.</p>
</blockquote>
<p>I am going to explain how to set up <code>FEDERATED</code> tables on our primary controlpanel server, called <code>cpsrv</code> below. The main database will be hosted on our primary mail server, called <code>mailsrv</code> below. I am not going to explain how to set up MySQL replication in this article.</p>
<h2 id="prepare-remote-host"><a href="#prepare-remote-host" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Prepare Remote Host</h2>
<p>On the remote host <code>mailsrv</code> that is going to host database <code>mailsync</code>, we need to set up a separate MySQL user which is going to be used for <code>FEDERATE</code> access:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-sql"><code><span class="token keyword">CREATE</span> <span class="token keyword">USER</span> mailsync<span class="token variable">@cpsrv</span> IDENTIFIED <span class="token keyword">BY</span> <span class="token string">'********'</span><span class="token punctuation">;</span>
<span class="token keyword">GRANT</span> <span class="token keyword">SELECT</span><span class="token punctuation">,</span> <span class="token keyword">INSERT</span><span class="token punctuation">,</span> <span class="token keyword">UPDATE</span><span class="token punctuation">,</span> <span class="token keyword">DELETE</span> <span class="token keyword">ON</span> <span class="token identifier"><span class="token punctuation">`</span>mailsync<span class="token punctuation">`</span></span><span class="token punctuation">.</span><span class="token operator">*</span> <span class="token keyword">TO</span> mailsync<span class="token variable">@cpsrv</span><span class="token punctuation">;</span>
</code></pre></div>
<h2 id="prepare-source-host"><a href="#prepare-source-host" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Prepare Source Host</h2>
<h3 id="enable-federated-storage-engine"><a href="#enable-federated-storage-engine" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Enable FEDERATED Storage Engine</h3>
<p>By default,  <a href="https://dev.mysql.com/doc/refman/8.0/en/federated-storage-engine.html" rel="nofollow noopener noreferrer" target="_blank">MySQL FEDERATED Storage Engine</a> is not enabled in MySQL. To enable <code>FEDERATED</code>, you must start the MySQL server using the <code>--federated</code> option. You can configure this in your <code>my.cnf</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">my.cnf</span><pre class="line-numbers language-ini"><code><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">mysqld</span><span class="token punctuation">]</span></span>
federated
</code></pre></div>
<p>After restarting MySQL, check if the engine is enabled:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-sql"><code>mysql<span class="token operator">></span> <span class="token keyword">SHOW</span> ENGINES
<span class="token operator">+</span><span class="token comment">--------------------+---------+</span>
<span class="token operator">|</span> <span class="token keyword">Engine</span>             <span class="token operator">|</span> Support <span class="token operator">|</span>
<span class="token operator">+</span><span class="token comment">--------------------+---------+</span>
<span class="token operator">|</span> FEDERATED          <span class="token operator">|</span> YES     <span class="token operator">|</span>
<span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span>
</code></pre></div>
<p>We only need to enable <code>FEDERATED</code> on the server that uses <code>FEDERATED</code> tables, not on the remote host where it connects to. In my case, I had to enable <code>FEDERATED</code> on <code>cpsrv</code>, but not on <code>mailsrv</code>.</p>
<h3 id="create-server"><a href="#create-server" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Create SERVER</h3>
<p>Prepare SERVER (user credentials for <code>mailsync</code> user connection to <code>mailsrv</code>) for <a href="https://dev.mysql.com/doc/refman/8.0/en/federated-storage-engine.html" rel="nofollow noopener noreferrer" target="_blank"><code>FEDERATED</code> Storage Engine</a> with <a href="https://dev.mysql.com/doc/refman/8.0/en/create-server.html" rel="nofollow noopener noreferrer" target="_blank"><code>CREATE SERVER</code></a> statement on <code>cpsrv</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-sql"><code><span class="token keyword">CREATE</span> SERVER mailsync
    <span class="token keyword">FOREIGN</span> <span class="token keyword">DATA</span> WRAPPER mysql
    OPTIONS <span class="token punctuation">(</span>HOST <span class="token string">'mailsrv'</span><span class="token punctuation">,</span> <span class="token keyword">DATABASE</span> <span class="token string">'mailsync'</span><span class="token punctuation">,</span> <span class="token keyword">USER</span> <span class="token string">'mailsync'</span><span class="token punctuation">,</span> PASSWORD <span class="token string">'********'</span><span class="token punctuation">)</span><span class="token punctuation">;</span>
</code></pre></div>
<p>You should then test the connection:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>cpsrv$ mysql <span class="token parameter variable">-u</span> mailsync <span class="token parameter variable">-h</span> mailsrv <span class="token parameter variable">-p</span> <span class="token parameter variable">-e</span> <span class="token string">'SELECT USER()'</span>
+----------------+
<span class="token operator">|</span> <span class="token environment constant">USER</span><span class="token punctuation">(</span><span class="token punctuation">)</span>         <span class="token operator">|</span>
+----------------+
<span class="token operator">|</span> mailsync@cpsrv <span class="token operator">|</span>
+----------------+
</code></pre></div>
<h2 id="setup-federated-tables"><a href="#setup-federated-tables" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Setup <code>FEDERATED</code> tables</h2>
<p>Assuming we already have set up the remote database <code>mailsync</code> on remote <code>mailsrv</code> and have a table schema from that whole database at hand. You should now copy that db schema <code>mailsync-schema.sql</code> to <code>mailsync-schema-federated.sql</code> and simply add <code>ENGINE=FEDERATED CONNECTION='mailsync/&#x3C;tablename>'</code> at the end of every <code>CREATE TABLE</code> statement, e.g.:</p>
<div class="nuxt-content-highlight"><span class="filename">mailsync-schema-federated.sql</span><pre class="line-numbers language-sql"><code><span class="token keyword">CREATE</span> <span class="token keyword">SCHEMA</span> <span class="token keyword">IF</span> <span class="token operator">NOT</span> <span class="token keyword">EXISTS</span> <span class="token identifier"><span class="token punctuation">`</span>mailsync<span class="token punctuation">`</span></span> <span class="token keyword">DEFAULT</span> <span class="token keyword">CHARACTER</span> <span class="token keyword">SET</span> utf8mb4 <span class="token keyword">COLLATE</span> utf8mb4_unicode_ci <span class="token punctuation">;</span>
<span class="token keyword">USE</span> <span class="token identifier"><span class="token punctuation">`</span>mailsync<span class="token punctuation">`</span></span> <span class="token punctuation">;</span>

<span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> <span class="token keyword">IF</span> <span class="token operator">NOT</span> <span class="token keyword">EXISTS</span> <span class="token identifier"><span class="token punctuation">`</span>mailmaps<span class="token punctuation">`</span></span> <span class="token punctuation">(</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">)</span>
<span class="token keyword">ENGINE</span> <span class="token operator">=</span> FEDERATED
CONNECTION <span class="token operator">=</span> <span class="token string">'mailsync/mailmaps'</span><span class="token punctuation">;</span>

<span class="token keyword">CREATE</span> <span class="token keyword">TABLE</span> <span class="token keyword">IF</span> <span class="token operator">NOT</span> <span class="token keyword">EXISTS</span> <span class="token identifier"><span class="token punctuation">`</span>mailaccounts<span class="token punctuation">`</span></span> <span class="token punctuation">(</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">.</span><span class="token punctuation">)</span>
<span class="token keyword">ENGINE</span> <span class="token operator">=</span> FEDERATED
CONNECTION <span class="token operator">=</span> <span class="token string">'mailsync/mailaccounts'</span><span class="token punctuation">;</span>
</code></pre></div>
<p>The <code>mailsync/</code> prefix in <code>CONNECTION</code> refers to the connection we have set up with <code>CREATE SERVER</code> (see above). You should use exactly the same table schema on both MySQL servers. And no, <code>FEDERATED</code> cannot detect the remote table schema, so you need to copy the whole <code>CREATE TABLE</code> field definitions. The schema on the source host will the just act as a skeleton to access the remote data.</p>
<p>Remember, no data of that <code>mailsync</code> database is ever stored on our source server <code>cpsrv</code>, so you could reload the whole database schema at any time like this without loosing any data:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>cpsrv$ mysql <span class="token parameter variable">-e</span> <span class="token string">'DROP DATABASE `mailsync`'</span>
cpsrv$ mysql <span class="token operator">&#x3C;</span> mailsync-schema-federated.sql
</code></pre></div>
<p>You can now <code>INSERT</code> data into this database and it will actually be inserted directly on the remote host <code>mailsrv</code>, all transparently and magically.</p>
<h2 id="possible-problems"><a href="#possible-problems" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Possible Problems</h2>
<h3 id="communication-link-failure"><a href="#communication-link-failure" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Communication link failure</h3>
<p>Up until MySQL 8.0.23, <code>FEDERATED</code> was working just perfectly without any single issue. Communication between the two hosts was super stable. But since MySQL 8.0.24, wet started to see sporadic errors on the source host like:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>Communication link failure: 1160 Got an error writing communication packets
Communication link failure: 1156 Got packets out of order
</code></pre></div>
<p>I was able to track this down to the <a href="https://dev.mysql.com/doc/refman/8.0/en/federated-storage-engine.html" rel="nofollow noopener noreferrer" target="_blank"><code>FEDERATED</code> Storage Engine</a> and the problem seems to be related to the newly introduced <a href="https://dev.mysql.com/doc/relnotes/mysql/8.0/en/news-8-0-24.html#mysqld-8-0-24-connection-management" rel="nofollow noopener noreferrer" target="_blank">connection management in MySQL 8.0.24</a>:</p>
<blockquote>
<p><strong>Connection Management Notes</strong></p>
<p>Previously, if a client did not use the connection to the server within the period specified by the <a href="https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_wait_timeout" rel="nofollow noopener noreferrer" target="_blank"><code>wait_timeout</code></a> system variable and the server closed the connection, the client received no notification of the reason. Typically, the client would see Lost connection to MySQL server during query (<a href="https://dev.mysql.com/doc/mysql-errors/8.0/en/client-error-reference.html#error_cr_server_lost" rel="nofollow noopener noreferrer" target="_blank"><code>CR_SERVER_LOST</code></a>) or MySQL server has gone away (<a href="https://dev.mysql.com/doc/mysql-errors/8.0/en/client-error-reference.html#error_cr_server_gone_error" rel="nofollow noopener noreferrer" target="_blank"><code>CR_SERVER_GONE_ERROR</code></a>).</p>
<p>In such cases, the server now writes the reason to the connection before closing it, and client receives a more informative error message, The client was disconnected by the server because of inactivity. See wait_timeout and interactive_timeout for configuring this behavior. (<a href="https://dev.mysql.com/doc/mysql-errors/8.0/en/server-error-reference.html#error_er_client_interaction_timeout" rel="nofollow noopener noreferrer" target="_blank"><code>ER_CLIENT_INTERACTION_TIMEOUT</code></a>).</p>
<p>The previous behavior still applies for client connections to older servers and connections to the server by older clients.</p>
</blockquote>
<p>I have reported this issue in Percona Community Forum:</p>
<ul>
<li><a href="https://forums.percona.com/t/mysql-8-0-24-8-0-25-connection-problems-with-federated-storage-engine-communication-link-failure/11487" rel="nofollow noopener noreferrer" target="_blank">MySQL 8.0.24 / 8.0.25 connection problems with <code>FEDERATED</code> storage engine (Communication link failure)</a></li>
</ul>
<p>Until now, the problem still is not resolved and I am still waiting for a response on that forum post. But setting <a href="https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_wait_timeout" rel="nofollow noopener noreferrer" target="_blank"><code>wait_timeout</code></a> high enough (raising it from default 8h to 24h) as a workaround almost made this issue disappear.</p>
<p>Set this on your remote host (the one your <code>FEDERATED</code> tables connect to):</p>
<div class="nuxt-content-highlight"><span class="filename">my.cnf</span><pre class="line-numbers language-ini"><code><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">mysqld</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">interactive_timeout</span>     <span class="token punctuation">=</span> <span class="token value attr-value">86400</span>
<span class="token key attr-name">wait_timeout</span>            <span class="token punctuation">=</span> <span class="token value attr-value">86400</span>
</code></pre></div>
<p>I will keep you posted here, if MySQL fixes this for good or if I find a real solution for it.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="mysql" term="mysql"/>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Recursively fetch dependent rows with mysqldump]]></title>
        <id>https://pipo.blog/articles/20210705-mysqldump-dependent-rows</id>
        <link href="https://pipo.blog/articles/20210705-mysqldump-dependent-rows"/>
        <updated>2021-07-05T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Ever wondered how to fetch a row from a MySQL database recursively, going through all foreign key (FK) constraints and fetch all dependent rows as well?]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/db-model-example.jpg"></p>
                      <p>How to fetch a row from a MySQL database recursively, going through all foreign key (FK) constraints and fetch all dependent rows as well? That's the question that bothered me during the last 20+ years as MySQL administrator. Isn't there a standard tool like some extended <code>mysqldump</code> that comes with that power? Short answer: No, there is no such tool. I gave up searching. It's just too complex to write a general-purpose tool that works for any kind of database schema.</p>
<!--more-->
<p><a href="https://stackoverflow.com/a/45967193/5982842" rel="nofollow noopener noreferrer" target="_blank">Bill Karwin</a> brings it to the point:</p>
<blockquote>
<p>It's sometimes easier to write a custom script just for your specific  schema, than for someone to write a general-purpose tool that works for  everyone's schema.</p>
</blockquote>
<p>Now you ask why we actually need this? Simple use case: You have accidentally deleted a customers record which resulted in the deletion of thousands related rows spread over 20 different tables. Restoring the full database from backup may not be an option as other customers have made tons of changing operations (DML like UPDATE/INSERT/DELETE) in the meantime, or restoring the full database would just result in a long downtime which is not acceptable.</p>
<p>So let's assume we can get a full database backup dump restored on some other MySQL server (maybe locally), where we can grab the relevant data from. We pick our favorite scripting language for such tasks, which is Python (really, if you don't use Python yet for such helper scripts, learn it! It's mostly a much better choice than using plain old Bash or a PHP script, Python is just simple and powerful and you can mostly do without any external libraries). We then end up with something like this:</p>
<blockquote>
<p><strong>IMPORTANT:</strong> Below code is just a snippet, an example of how you could accomplish this for your own db schema. It is not an all-purpose script that can be used out of the box. Make sure, you define your own queries for every related table.</p>
<p>I also assume you have correctly configured your db credentials in <code>.my.cnf</code> or <code>.mylogin.cnf</code> (using <code>mysql_config_editor</code>), so you don't need to provide username/password to <code>mysqldump</code>.</p>
</blockquote>
<div class="nuxt-content-highlight"><span class="filename">mysql-recursive-dump.py </span><pre class="line-numbers language-python"><code><span class="token comment">#!/usr/bin/env python3</span>
<span class="token comment">#</span>
<span class="token comment"># Copyright (c) 2021 Philip Iezzi, Onlime GmbH - https://www.onlime.ch</span>
<span class="token comment">#</span>

<span class="token keyword">import</span> argparse
<span class="token keyword">import</span> subprocess

header <span class="token operator">=</span> <span class="token triple-quoted-string string">"""SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT;
SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS;
SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION;
SET NAMES utf8mb4;
SET @OLD_TIME_ZONE=@@TIME_ZONE;
SET TIME_ZONE='+00:00';
SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0;
SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0;
SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO';
SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0;
"""</span>
footer <span class="token operator">=</span> <span class="token triple-quoted-string string">"""SET TIME_ZONE=@OLD_TIME_ZONE
SET SQL_MODE=@OLD_SQL_MODE;
SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS;
SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS;
SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT;
SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS;
SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION;
SET SQL_NOTES=@OLD_SQL_NOTES;
"""</span>

<span class="token keyword">def</span> <span class="token function">prepend_line</span><span class="token punctuation">(</span>filename<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> line<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token keyword">with</span> <span class="token builtin">open</span><span class="token punctuation">(</span>filename<span class="token punctuation">,</span> <span class="token string">'r'</span><span class="token punctuation">)</span> <span class="token keyword">as</span> f<span class="token punctuation">:</span>
        data <span class="token operator">=</span> f<span class="token punctuation">.</span>read<span class="token punctuation">(</span><span class="token punctuation">)</span>
    <span class="token keyword">with</span> <span class="token builtin">open</span><span class="token punctuation">(</span>filename<span class="token punctuation">,</span> <span class="token string">'w'</span><span class="token punctuation">)</span> <span class="token keyword">as</span> f<span class="token punctuation">:</span>
        f<span class="token punctuation">.</span>write<span class="token punctuation">(</span>line <span class="token operator">+</span> data<span class="token punctuation">)</span>

<span class="token keyword">def</span> <span class="token function">append_line</span><span class="token punctuation">(</span>filename<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> line<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
    <span class="token keyword">with</span> <span class="token builtin">open</span><span class="token punctuation">(</span>filename<span class="token punctuation">,</span> <span class="token string">'a'</span><span class="token punctuation">)</span> <span class="token keyword">as</span> fd<span class="token punctuation">:</span>
        fd<span class="token punctuation">.</span>write<span class="token punctuation">(</span>line<span class="token punctuation">)</span>

<span class="token keyword">class</span> <span class="token class-name">RecursiveDumper</span><span class="token punctuation">:</span>

    <span class="token keyword">def</span> <span class="token function">__init__</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> database<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> model<span class="token punctuation">:</span> <span class="token builtin">str</span><span class="token punctuation">,</span> <span class="token builtin">id</span><span class="token punctuation">:</span> <span class="token builtin">int</span><span class="token punctuation">,</span> dryrun<span class="token punctuation">:</span> <span class="token builtin">bool</span> <span class="token operator">=</span> <span class="token boolean">False</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
        self<span class="token punctuation">.</span>dumpfile <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f'dump-</span><span class="token interpolation"><span class="token punctuation">{</span>model<span class="token punctuation">}</span></span><span class="token string">-</span><span class="token interpolation"><span class="token punctuation">{</span><span class="token builtin">id</span><span class="token punctuation">}</span></span><span class="token string">.sql'</span></span>
        self<span class="token punctuation">.</span>database <span class="token operator">=</span> database
        self<span class="token punctuation">.</span>model <span class="token operator">=</span> model
        self<span class="token punctuation">.</span><span class="token builtin">id</span> <span class="token operator">=</span> <span class="token builtin">id</span>
        self<span class="token punctuation">.</span>dryrun <span class="token operator">=</span> dryrun

    <span class="token keyword">def</span> <span class="token function">dump</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span>
        self<span class="token punctuation">.</span>dump_customer<span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token keyword">if</span> self<span class="token punctuation">.</span>model <span class="token operator">==</span> <span class="token string">'customer'</span> <span class="token keyword">else</span> self<span class="token punctuation">.</span>dump_webabo<span class="token punctuation">(</span><span class="token punctuation">)</span>

    <span class="token keyword">def</span> <span class="token function">dump_customer</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span>
        custid <span class="token operator">=</span> self<span class="token punctuation">.</span><span class="token builtin">id</span>
        customers_query <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f'customer_id = </span><span class="token interpolation"><span class="token punctuation">{</span>custid<span class="token punctuation">}</span></span><span class="token string">'</span></span>
        webabos_query <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f'webabo_id IN (SELECT id FROM webabos WHERE </span><span class="token interpolation"><span class="token punctuation">{</span>customers_query<span class="token punctuation">}</span></span><span class="token string">)'</span></span>
        <span class="token comment"># ... (stripped down)</span>
        queries <span class="token operator">=</span> <span class="token punctuation">{</span>
            <span class="token string">'customers'</span><span class="token punctuation">:</span> <span class="token string-interpolation"><span class="token string">f'id = </span><span class="token interpolation"><span class="token punctuation">{</span>custid<span class="token punctuation">}</span></span><span class="token string">'</span></span><span class="token punctuation">,</span>
            <span class="token string">'webabos'</span><span class="token punctuation">:</span> <span class="token string-interpolation"><span class="token string">f'customer_id = </span><span class="token interpolation"><span class="token punctuation">{</span>custid<span class="token punctuation">}</span></span><span class="token string">'</span></span><span class="token punctuation">,</span>
            <span class="token string">'subdomains'</span><span class="token punctuation">:</span> webabos_query<span class="token punctuation">,</span>
            <span class="token string">'mailaccounts'</span><span class="token punctuation">:</span> webabos_query<span class="token punctuation">,</span>
            <span class="token comment"># ...</span>
        <span class="token punctuation">}</span>
        self<span class="token punctuation">.</span>run_dumps<span class="token punctuation">(</span>queries<span class="token punctuation">)</span>

    <span class="token keyword">def</span> <span class="token function">dump_webabo</span><span class="token punctuation">(</span>self<span class="token punctuation">)</span><span class="token punctuation">:</span>
        webaboid <span class="token operator">=</span> self<span class="token punctuation">.</span><span class="token builtin">id</span>
        webabos_query <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f'webabo_id = </span><span class="token interpolation"><span class="token punctuation">{</span>webaboid<span class="token punctuation">}</span></span><span class="token string">'</span></span>
        subdomains_query <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f'subdomain_id IN (SELECT id FROM subdomains WHERE </span><span class="token interpolation"><span class="token punctuation">{</span>webabos_query<span class="token punctuation">}</span></span><span class="token string">)'</span></span>
        addondomains_query <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f'addondomain_id IN (SELECT id FROM addondomains WHERE </span><span class="token interpolation"><span class="token punctuation">{</span>webabos_query<span class="token punctuation">}</span></span><span class="token string"> OR </span><span class="token interpolation"><span class="token punctuation">{</span>subdomains_query<span class="token punctuation">}</span></span><span class="token string">)'</span></span>
        dnszones_query <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f'</span><span class="token interpolation"><span class="token punctuation">{</span>webabos_query<span class="token punctuation">}</span></span><span class="token string"> OR </span><span class="token interpolation"><span class="token punctuation">{</span>addondomains_query<span class="token punctuation">}</span></span><span class="token string">'</span></span>
        <span class="token comment"># ... (stripped down)</span>
        queries <span class="token operator">=</span> <span class="token punctuation">{</span>
            <span class="token string">'webabos'</span><span class="token punctuation">:</span> <span class="token string-interpolation"><span class="token string">f'id = </span><span class="token interpolation"><span class="token punctuation">{</span>webaboid<span class="token punctuation">}</span></span><span class="token string">'</span></span><span class="token punctuation">,</span>
            <span class="token string">'logins'</span><span class="token punctuation">:</span> webabos_query<span class="token punctuation">,</span>
            <span class="token string">'subdomains'</span><span class="token punctuation">:</span> webabos_query<span class="token punctuation">,</span>
            <span class="token string">'mailaccounts'</span><span class="token punctuation">:</span> webabos_query<span class="token punctuation">,</span>
            <span class="token comment"># ...</span>
        <span class="token punctuation">}</span>
        self<span class="token punctuation">.</span>run_dumps<span class="token punctuation">(</span>queries<span class="token punctuation">)</span>
    
    <span class="token keyword">def</span> <span class="token function">run_dumps</span><span class="token punctuation">(</span>self<span class="token punctuation">,</span> queries<span class="token punctuation">:</span> <span class="token builtin">object</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
        <span class="token keyword">with</span> <span class="token builtin">open</span><span class="token punctuation">(</span>self<span class="token punctuation">.</span>dumpfile<span class="token punctuation">,</span> <span class="token string">'w'</span><span class="token punctuation">)</span> <span class="token keyword">as</span> f<span class="token punctuation">:</span>
            <span class="token keyword">for</span> table<span class="token punctuation">,</span> where <span class="token keyword">in</span> queries<span class="token punctuation">.</span>items<span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">:</span>
                cmd <span class="token operator">=</span> <span class="token string-interpolation"><span class="token string">f"mysqldump --skip-extended-insert --skip-triggers --replace --compact --no-create-info --lock-all-tables --where '</span><span class="token interpolation"><span class="token punctuation">{</span>where<span class="token punctuation">}</span></span><span class="token string">' </span><span class="token interpolation"><span class="token punctuation">{</span>self<span class="token punctuation">.</span>database<span class="token punctuation">}</span></span><span class="token string"> </span><span class="token interpolation"><span class="token punctuation">{</span>table<span class="token punctuation">}</span></span><span class="token string">"</span></span>
                <span class="token keyword">print</span><span class="token punctuation">(</span>cmd<span class="token punctuation">)</span> <span class="token keyword">if</span> self<span class="token punctuation">.</span>dryrun <span class="token keyword">else</span> subprocess<span class="token punctuation">.</span>run<span class="token punctuation">(</span>cmd<span class="token punctuation">,</span> stdout<span class="token operator">=</span>f<span class="token punctuation">,</span> shell<span class="token operator">=</span><span class="token boolean">True</span><span class="token punctuation">)</span>
        prepend_line<span class="token punctuation">(</span>self<span class="token punctuation">.</span>dumpfile<span class="token punctuation">,</span> header<span class="token punctuation">)</span>
        append_line<span class="token punctuation">(</span>self<span class="token punctuation">.</span>dumpfile<span class="token punctuation">,</span> footer<span class="token punctuation">)</span>


<span class="token keyword">if</span> __name__ <span class="token operator">==</span> <span class="token string">"__main__"</span><span class="token punctuation">:</span>
    parser <span class="token operator">=</span> argparse<span class="token punctuation">.</span>ArgumentParser<span class="token punctuation">(</span><span class="token punctuation">)</span>
    parser<span class="token punctuation">.</span>add_argument<span class="token punctuation">(</span><span class="token string">'model'</span><span class="token punctuation">,</span> choices<span class="token operator">=</span><span class="token punctuation">[</span><span class="token string">'customer'</span><span class="token punctuation">,</span> <span class="token string">'webabo'</span><span class="token punctuation">]</span><span class="token punctuation">,</span> <span class="token builtin">help</span><span class="token operator">=</span><span class="token string">'Object to dump'</span><span class="token punctuation">)</span>
    parser<span class="token punctuation">.</span>add_argument<span class="token punctuation">(</span><span class="token string">'id'</span><span class="token punctuation">,</span> <span class="token builtin">type</span><span class="token operator">=</span><span class="token builtin">int</span><span class="token punctuation">,</span> <span class="token builtin">help</span><span class="token operator">=</span><span class="token string">'Customer or webabo ID'</span><span class="token punctuation">)</span>
    parser<span class="token punctuation">.</span>add_argument<span class="token punctuation">(</span><span class="token string">'--database'</span><span class="token punctuation">,</span> <span class="token builtin">type</span><span class="token operator">=</span><span class="token builtin">str</span><span class="token punctuation">,</span> default<span class="token operator">=</span><span class="token string">'mydb'</span><span class="token punctuation">,</span> <span class="token builtin">help</span><span class="token operator">=</span><span class="token string">'Airpane database'</span><span class="token punctuation">)</span>
    parser<span class="token punctuation">.</span>add_argument<span class="token punctuation">(</span><span class="token string">'--dryrun'</span><span class="token punctuation">,</span> <span class="token string">'--dry-run'</span><span class="token punctuation">,</span> action<span class="token operator">=</span><span class="token string">'store_true'</span><span class="token punctuation">,</span> <span class="token builtin">help</span><span class="token operator">=</span><span class="token string">'Dry-run without dumping any data'</span><span class="token punctuation">)</span>
    args <span class="token operator">=</span> parser<span class="token punctuation">.</span>parse_args<span class="token punctuation">(</span><span class="token punctuation">)</span>

    dumper <span class="token operator">=</span> RecursiveDumper<span class="token punctuation">(</span>args<span class="token punctuation">.</span>database<span class="token punctuation">,</span> args<span class="token punctuation">.</span>model<span class="token punctuation">,</span> args<span class="token punctuation">.</span><span class="token builtin">id</span><span class="token punctuation">,</span> args<span class="token punctuation">.</span>dryrun<span class="token punctuation">)</span>
    dumper<span class="token punctuation">.</span>dump<span class="token punctuation">(</span><span class="token punctuation">)</span>
</code></pre></div>
<p>This script generates a dump of all related data using selects and subselects in <code>mysqldump --where</code>. All data is merged into a single dump which contains header/footer statements that ensure the data can be loaded correctly - most important turning off FK constraints during the restore process.</p>
<p>The heart of that script are simple <code>mysqldump</code> statements that look like this:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ mysqldump --skip-extended-insert --skip-triggers <span class="token parameter variable">--replace</span> <span class="token parameter variable">--compact</span> --no-create-info --lock-all-tables <span class="token parameter variable">--where</span> <span class="token string">'&#x3C;where_condition>'</span> <span class="token operator">&#x3C;</span>db_name<span class="token operator">></span> <span class="token operator">&#x3C;</span>tbl_name<span class="token operator">></span>"
</code></pre></div>
<p>Explanation:</p>
<ul>
<li><code>--skip-extended-inserts</code> (optional): This could be omitted, but we prefer having one <code>INSERT</code> statement per line for easier review of the full dump.</li>
<li><code>--skip-triggers</code>: Don't dump any trigger information as we simply want to restore data.</li>
<li><code>--replace</code> (optional): Write <code>REPLACE</code> statements rather than <code>INSERT</code> statements - just in case we're dumping a bit too much that may have not been deleted by our accidential fatal deletion action.</li>
<li><code>--compact</code>: Only dump <code>INSERT</code> lines, no extras, as we merge all dumps together into one file and care about header/footer statements by ourselves.</li>
<li><code>--no-create-info</code>: Do not write <code>CREATE TABLE</code> statements that create each dumped table.</li>
<li><code>--lock-all-tables</code>: This is needed as we want to use the power of subselects in <code>--where</code> conditions.</li>
<li><code>--where</code>: Dump only rows selected by the given <code>WHERE</code> condition.</li>
</ul>
<p>The above script could be used like this:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ ./backup-data-dump.py <span class="token parameter variable">--help</span>
usage: backup-data-dump.py <span class="token punctuation">[</span>-h<span class="token punctuation">]</span> <span class="token punctuation">[</span>--database DATABASE<span class="token punctuation">]</span> <span class="token punctuation">[</span>--dryrun<span class="token punctuation">]</span> <span class="token punctuation">{</span>customer,webabo<span class="token punctuation">}</span> <span class="token function">id</span>

$ ./backup-data-dump.py customer <span class="token number">1234</span> <span class="token parameter variable">--dryrun</span>
<span class="token comment"># only prints out mysqldump commands without dumping any data</span>

$ ./backup-data-dump.py customer <span class="token number">1234</span>
<span class="token comment"># produces: dump-customer-1234.sql</span>

$ ./backup-data-dump.py webabo <span class="token number">999</span>
<span class="token comment"># produces: dump-webabo-999.sql</span>
</code></pre></div>
<p>After reviewing the dump, you could upload it to your production server and restore data from it:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ mysql db_name <span class="token operator">&#x3C;</span> dump-customer-1234.sql
</code></pre></div>
<p>In case you have triggers set up on any affected table you're restoring data to, consider removing them before the restore and re-creating them right afterwards.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="mysql" term="mysql"/>
        <category label="python" term="python"/>
        <category label="sysadmin" term="sysadmin"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Fail2ban persistent banning]]></title>
        <id>https://pipo.blog/articles/20210518-fail2ban-persistent-banning</id>
        <link href="https://pipo.blog/articles/20210518-fail2ban-persistent-banning"/>
        <updated>2021-05-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Persistent IP banning using Fail2ban's recidive jail.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/fail2ban.jpg"></p>
                      <p>If you are using <a href="https://www.fail2ban.org" rel="nofollow noopener noreferrer" target="_blank">Fail2ban</a>, there is no standard recommended way to persistently ban IPs. Some people recommend to do this outside of Fail2ban, using e.g. <a href="https://packages.debian.org/stable/iptables-persistent" rel="nofollow noopener noreferrer" target="_blank">iptables-persistent</a>, which is actually super easy to install and configure. But let's say, we don't want to install any extras and want to accomplish the same with Fail2ban, as we already have fail2ban on every single host (which is a must!).</p>
<!--more-->
<p>Google for "fail2ban ban ip persistently" lead me to the following interesting solutions:</p>
<ul>
<li><a href="https://arno0x0x.wordpress.com/2015/12/30/fail2ban-permanent-persistent-bans/" rel="nofollow noopener noreferrer" target="_blank">Configure Fail2Ban for permanent and persistent bans</a></li>
<li><a href="https://dev-notes.eu/2018/04/persistent-banning-of-ip-addresses-with-fail2ban/" rel="nofollow noopener noreferrer" target="_blank">Persistent Banning of IP Addresses with Fail2Ban</a></li>
</ul>
<p>But this did not work out for me. The thing is, if we extend <code>actionstart</code> in  <code>action.d/iptables-multiport.conf</code> (or <code>iptables-multiport.local</code> override) as recommended in above tutorials, that is not going to add any IPs on a Fail2ban restart, but only once the first IP gets added to any jail.</p>
<p>So these are my two proposed solutions:</p>
<h2 id="using-recidive-jail-ban-for-1-week"><a href="#using-recidive-jail-ban-for-1-week" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Using recidive jail (ban for 1 week)</h2>
<p>The provided <code>recidive</code> jail/filter monitors the fail2ban log file, and enables you to add long time bans for ip addresses that get banned by fail2ban multiple times. Default configuration looks like this:</p>
<div class="nuxt-content-highlight"><span class="filename">jail.conf</span><pre class="line-numbers language-ini"><code><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">recidive</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">logpath</span>  <span class="token punctuation">=</span> <span class="token value attr-value">/var/log/fail2ban.log</span>
<span class="token key attr-name">banaction</span> <span class="token punctuation">=</span> <span class="token value attr-value">%(banaction_allports)s</span>
<span class="token key attr-name">bantime</span>  <span class="token punctuation">=</span> <span class="token value attr-value">1w</span>
<span class="token key attr-name">findtime</span> <span class="token punctuation">=</span> <span class="token value attr-value">1d</span>
</code></pre></div>
<p>Simply enable this jail, e.g. in <code>jail.d/custom.conf</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">jail.d/custom.conf</span><pre class="line-numbers language-ini"><code><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">recidive</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">enabled</span> <span class="token punctuation">=</span> <span class="token value attr-value">true</span>
</code></pre></div>
<p>We could then ban an IP manually for a whole week by adding it to that jail:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ fail2ban-client <span class="token builtin class-name">set</span> recidive banip <span class="token number">11.22</span>.33.44
</code></pre></div>
<p>verify:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ iptables <span class="token parameter variable">-S</span> f2b-recidive
<span class="token parameter variable">-N</span> f2b-recidive
<span class="token parameter variable">-A</span> f2b-recidive <span class="token parameter variable">-s</span> <span class="token number">11.22</span>.33.44/32 <span class="token parameter variable">-j</span> REJECT --reject-with icmp-port-unreachable
<span class="token parameter variable">-A</span> f2b-recidive <span class="token parameter variable">-j</span> RETURN
</code></pre></div>
<h2 id="using-manual-jail-ban-forever"><a href="#using-manual-jail-ban-forever" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Using manual jail (ban forever)</h2>
<p>Let's configured a custom <code>manual</code> jail for manual banning:</p>
<div class="nuxt-content-highlight"><span class="filename">filter.d/manual.conf</span><pre class="line-numbers language-ini"><code><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">Definition</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">failregex</span> <span class="token punctuation">=</span> 
<span class="token key attr-name">ignoreregex</span> <span class="token punctuation">=</span> 
</code></pre></div>
<div class="nuxt-content-highlight"><span class="filename">jail.d/custom.conf</span><pre class="line-numbers language-ini"><code><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">manual</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">banaction</span> <span class="token punctuation">=</span> <span class="token value attr-value">%(banaction_allports)s</span>
<span class="token key attr-name">bantime</span> <span class="token punctuation">=</span> <span class="token value attr-value">-1</span>
<span class="token key attr-name">enabled</span> <span class="token punctuation">=</span> <span class="token value attr-value">true</span>
</code></pre></div>
<p>Every jail needs a filter. But as we don't need that jail to parse any logfile, we have simply set up a dummy filter definition.</p>
<p>We could ban an IP manually forever by adding it to that jail:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ fail2ban-client <span class="token builtin class-name">set</span> manual banip <span class="token number">11.22</span>.33.44
</code></pre></div>
<p>And remember the following command if you wish to ever unban that IP:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ fail2ban-client <span class="token builtin class-name">set</span> manual unbanip <span class="token number">11.22</span>.33.44
</code></pre></div>
<p>Loading a whole blocklist could be done like this:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">grep</span> <span class="token parameter variable">-v</span> <span class="token string">'^#'</span> ip.blocklist <span class="token operator">|</span> <span class="token keyword">while</span> <span class="token builtin class-name">read</span> IP<span class="token punctuation">;</span> <span class="token keyword">do</span> fail2ban-client <span class="token builtin class-name">set</span> manual banip <span class="token variable">$IP</span><span class="token punctuation">;</span> <span class="token keyword">done</span>
</code></pre></div>
<p>You could then add this to some blocklist loading script, and triggering it e.g. by using <code>ExecStartPost</code> in a Systemd unit file override. But actually, that should not be needed, as Fail2ban persists its state and keeps banned IPs after a Fail2ban restart or even after a full system reboot.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="security" term="security"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Block email forwarding spam with Rspamd]]></title>
        <id>https://pipo.blog/articles/20210416-rspamd-forwarding-spam</id>
        <link href="https://pipo.blog/articles/20210416-rspamd-forwarding-spam"/>
        <updated>2021-04-16T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Apply spam filtering to outgoing email forwardings with Rspamd.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/spam-fries.jpg"></p>
                      <p>At <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime GmbH</a> we have a mail infrastructure that consists of 3 mail servers: <code>mx1</code>  acts as primary MX server and provides SMTP as outgoing mail gateway for our customers. <code>mx2</code> acts as secondary MX and fallback incoming mailserver. Incoming email from <code>mx2</code> is forwarded to <code>mx1</code> which does spam/antivirus filtering with <a href="https://rspamd.com/" rel="nofollow noopener noreferrer" target="_blank">Rspamd</a>. Finally, the 3rd mailserver which is simply called <code>mail</code> acts as IMAP server and outgoing mailserver.</p>
<p>We allow customers to set up email forwardings on their domains. A forwarding address could have another customer email address as destination, but could also directly or indirectly (through another forwarding) point to an external email address. Spam filtering is done on <code>mx1</code>, but Rspamd actually just flags the email as ham/spam with a spam score and adds the <code>X-Spamd-Result</code> header with all symbols. The actual action is done on the final mailserver <code>mail</code> in the recipients mailbox via Sieve rule. The reason for this is that we want to let the customer define his own spam score (going from "minimal" to "radical" which maps to a spam score treshold) and what should happen with an email that got classified as spam (store it in <code>Spam</code> box or discard it directly).</p>
<!--more-->
<p>So, how can we block an incoming spam email that would get forwarded to some external address, without introducing any <a href="https://en.wikipedia.org/wiki/Backscatter_(email)" rel="nofollow noopener noreferrer" target="_blank">Backscattering</a>? We need to block incoming email already radically on the first/incoming mail server <code>mx1</code> if it will be forwarded to some external email address.</p>
<h2 id="mail-infrastructure-recap"><a href="#mail-infrastructure-recap" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Mail Infrastructure Recap</h2>
<p>If you didn't quite get the description of our mailserver infrastructure from my intro above, I try to explain in some more visual way:</p>
<p><img alt="2021-mail-infra-overview" src="/images/articles/20210416-rspamd-forwarding-spam/2021-mail-infra-overview.png"></p>
<p>So, again, Rspamd on <code>mx1</code> just flags the messages with a spam score, but the actual blocking of spam is done on <code>mail</code> through a Sieve rule in the final mailbox (on <a href="https://dovecot.org/" rel="nofollow noopener noreferrer" target="_blank">Dovecot IMAP server</a>). If we would block an outgoing forwarding spam mail there, that would result in backscattering, as the email was not rejected with a <code>5xx</code> status code on the first mailserver <code>mx1</code>. How can we block it already on <code>mx1</code> without introducing any added complexity in our mail infrastructure? See the problem?</p>
<h2 id="the-idea"><a href="#the-idea" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>The Idea</h2>
<p>Proposed solution:</p>
<ul>
<li>Deploy a list of forwarding emails (that point directly or indirectly to some external address) to a Rspamd map <code>forwardings.inc.local</code></li>
<li>Use <a href="https://rspamd.com/doc/modules/multimap.html" rel="nofollow noopener noreferrer" target="_blank">Rspamd mutimap module</a> to tag all emails with <code>rcpt</code> (map type) in <code>forwardings.inc.local</code> with symbol <code>IS_FORWARDING</code></li>
<li>Use <a href="https://rspamd.com/doc/modules/force_actions.html" rel="nofollow noopener noreferrer" target="_blank">Rspamd force actions</a> to override default action with <code>reject</code> if the <code>IS_FORWARDING</code> symbol is set and <code>X-Spam: Yes</code> header was set</li>
</ul>
<p>Like this, we would reject directly on mx1/mx2/mailman as frontend mailservers, not generating any bounces / backscattering!</p>
<h2 id="rspamd-force-action"><a href="#rspamd-force-action" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Rspamd force action</h2>
<p>That solution was quite easy to implement. The only tricky part was to generate a list of forwarding email addresses that pointed directly or indirectly (potentially recursively over multiple forwardings) to some external address. This is your job on application level. I put this list into Rspamd's <code>local.d/maps.d/forwardings.inc.local</code> and keep it updated through our controlpanel, deploying it via an agent/microservice on the frontend mailservers. Whenever a customer creates a new forwarding that points to some external email address, this list is regenerated and deployed to the frontend mailservers <code>mx1/mx2/mailman</code>. It contains one address per line:</p>
<div class="nuxt-content-highlight"><span class="filename">local.d/maps.d/forwardings.inc.local</span><pre class="line-numbers language-text"><code>forwarding1@example.com
forwarding2@example.com
...
</code></pre></div>
<p>Rspamd <a href="https://rspamd.com/doc/modules/multimap.html" rel="nofollow noopener noreferrer" target="_blank">multimap module</a> now adds symbol <code>IS_FORWARDING</code> (which should be unterstood as "is forwarding to some external recipient") if the recipient was found in <code>forwardings.inc.local</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">local.d/multimap.conf</span><pre class="line-numbers language-ruby"><code><span class="token constant">IS_FORWARDING</span> <span class="token punctuation">{</span>
    type <span class="token operator">=</span> <span class="token string-literal"><span class="token string">"rcpt"</span></span><span class="token punctuation">;</span>
    map <span class="token operator">=</span> <span class="token string-literal"><span class="token string">"/etc/rspamd/local.d/maps.d/forwardings.inc.local"</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>In case you were wondering: No, it is not required to restart/reload Rspamd when this map content changes!</p>
<p>Finally, we configure the Rspamd <a href="https://rspamd.com/doc/modules/force_actions.html" rel="nofollow noopener noreferrer" target="_blank">force actions module</a> in <code>force_actions.conf</code> as follows:</p>
<div class="nuxt-content-highlight"><span class="filename">local.d/force_actions.conf</span><pre class="line-numbers language-ruby"><code>rules <span class="token punctuation">{</span>
  <span class="token constant">SPAMMY_FORWARDING</span> <span class="token punctuation">{</span>
    action <span class="token operator">=</span> <span class="token string-literal"><span class="token string">"reject"</span></span><span class="token punctuation">;</span>
    expression <span class="token operator">=</span> <span class="token string-literal"><span class="token string">"IS_FORWARDING"</span></span><span class="token punctuation">;</span>
    <span class="token comment"># require_action setting defines actions that will be overridden</span>
    require_action <span class="token operator">=</span> <span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"add header"</span></span><span class="token punctuation">]</span><span class="token punctuation">;</span>
    <span class="token comment"># default message: "Spam message rejected"</span>
    <span class="token comment">#message = "This message cannot be sent because it looks like spam."</span>
  <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>You could also override the default message "Spam message rejected" there, see my comment.</p>
<blockquote>
<p><strong>Further explanation:</strong> The <code>X-Spam: Yes</code> header is set by Rspamd if the calculated spam score was above the default <code>add_header = 6.0</code> action configuration. Above force action only comes into play, if that header was set AND the <code>IS_FORWARDING</code> symol is there. And because we didn't define any score for <code>IS_FORWARDING</code> symbol, it will just be <code>0.00</code>, not influencing the whole spam score calculation. Think of this symbol just as a flag, not as any spam/ham symbol.</p>
</blockquote>
<p>That's the whole trick!</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="rspamd" term="rspamd"/>
        <category label="email" term="email"/>
        <category label="sysadmin" term="sysadmin"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[MySQL MyISAM to InnoDB Conversion]]></title>
        <id>https://pipo.blog/articles/20201222-mysql-myisam-to-innodb</id>
        <link href="https://pipo.blog/articles/20201222-mysql-myisam-to-innodb"/>
        <updated>2020-12-22T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How to convert thousands of MySQL tables from MyISAM to InnoDB.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/mysql-storage-engines.png"></p>
                      <p>Back in November 2020, I managed to convert all legacy MyISAM tables to InnoDB on all <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime GmbH</a> database servers and customer webservers. MyISAM as legacy storage engine was quite okay-ish on MySQL 5.7 but started to perform really bad on MySQL 8.0. There was simply no reason to keep on using it and honestly, for the last 10 years I did never understand why people still held onto it. I had to find out that a lot of my customers just never heard of any storage engine types, and they didn't even know of any differences between <a href="https://dev.mysql.com/doc/refman/8.0/en/myisam-storage-engine.html" rel="nofollow noopener noreferrer" target="_blank">MyISAM</a> and <a href="https://dev.mysql.com/doc/refman/8.0/en/innodb-storage-engine.html" rel="nofollow noopener noreferrer" target="_blank">InnoDB</a>.</p>
<!--more-->
<h2 id="list-all-myisam-tables"><a href="#list-all-myisam-tables" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>List all MyISAM tables</h2>
<p>Before proceeding to the actual task of converting all those shitty MyISAM tables, I would like to present you some queries to find out about MyISAM table existence on your MySQL server.</p>
<p>List all <code>MyISAM</code> tables (+ table size) of all user databases:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-sql"><code><span class="token keyword">SELECT</span> table_schema db<span class="token punctuation">,</span> table_name tbl<span class="token punctuation">,</span> CAST<span class="token punctuation">(</span><span class="token punctuation">(</span>IFNULL<span class="token punctuation">(</span>data_length<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span> <span class="token operator">+</span> IFNULL<span class="token punctuation">(</span>index_length<span class="token punctuation">,</span> <span class="token number">0</span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token keyword">AS</span> SIGNED<span class="token punctuation">)</span> size 
    <span class="token keyword">FROM</span> information_schema<span class="token punctuation">.</span><span class="token keyword">TABLES</span>
        <span class="token keyword">WHERE</span> <span class="token keyword">engine</span> <span class="token operator">=</span> <span class="token string">'MyISAM'</span> <span class="token operator">AND</span> table_type <span class="token operator">=</span> <span class="token string">'BASE TABLE'</span>
            <span class="token operator">AND</span> table_schema <span class="token operator">NOT</span> <span class="token operator">IN</span> <span class="token punctuation">(</span><span class="token string">'information_schema'</span><span class="token punctuation">,</span> <span class="token string">'sys'</span><span class="token punctuation">,</span> <span class="token string">'performance_schema'</span><span class="token punctuation">,</span> <span class="token string">'mysql'</span><span class="token punctuation">)</span>
                <span class="token keyword">ORDER</span> <span class="token keyword">BY</span> table_schema<span class="token punctuation">,</span> table_name<span class="token punctuation">;</span>
</code></pre></div>
<p>Only list databases with at least one <code>MyISAM</code> table:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-sql"><code><span class="token keyword">SELECT</span> table_schema db <span class="token keyword">FROM</span> information_schema<span class="token punctuation">.</span><span class="token keyword">TABLES</span>
    <span class="token keyword">WHERE</span> <span class="token keyword">engine</span> <span class="token operator">=</span> <span class="token string">'MyISAM'</span> <span class="token operator">AND</span> table_type <span class="token operator">=</span> <span class="token string">'BASE TABLE'</span>
        <span class="token operator">AND</span> table_schema <span class="token operator">NOT</span> <span class="token operator">IN</span> <span class="token punctuation">(</span><span class="token string">'information_schema'</span><span class="token punctuation">,</span> <span class="token string">'sys'</span><span class="token punctuation">,</span> <span class="token string">'performance_schema'</span><span class="token punctuation">,</span> <span class="token string">'mysql'</span><span class="token punctuation">)</span>
            <span class="token keyword">GROUP</span> <span class="token keyword">BY</span> table_schema <span class="token keyword">ORDER</span> <span class="token keyword">BY</span> table_schema<span class="token punctuation">;</span>
</code></pre></div>
<p>Or do it all with a nice one-liner for plain output (batch mode), one db name per line:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token comment"># full list of all tables</span>
$ mysql <span class="token parameter variable">-sN</span> <span class="token parameter variable">-r</span> <span class="token parameter variable">-e</span> <span class="token string">"SELECT CONCAT(table_schema, '.', table_name) FROM information_schema.TABLES 
    WHERE engine = 'MyISAM' AND table_type = 'BASE TABLE'
        AND table_schema NOT IN ('information_schema', 'sys', 'performance_schema', 'mysql')
            ORDER BY table_schema, table_name"</span>

<span class="token comment"># only list databases</span>
$ mysql <span class="token parameter variable">-sN</span> <span class="token parameter variable">-r</span> <span class="token parameter variable">-e</span> <span class="token string">"SELECT table_schema db FROM information_schema.TABLES
    WHERE engine = 'MyISAM' AND table_type = 'BASE TABLE'
        AND table_schema NOT IN ('information_schema', 'sys', 'performance_schema', 'mysql')
            GROUP BY table_schema ORDER BY table_schema"</span>
</code></pre></div>
<h2 id="convert-single-table-to-innodb"><a href="#convert-single-table-to-innodb" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Convert single table to InnoDB</h2>
<p>To convert a single table to InnoDB, run:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-sql"><code><span class="token keyword">ALTER</span> <span class="token keyword">TABLE</span> <span class="token identifier"><span class="token punctuation">`</span>db<span class="token punctuation">`</span></span><span class="token punctuation">.</span><span class="token identifier"><span class="token punctuation">`</span>tablename<span class="token punctuation">`</span></span> <span class="token keyword">ENGINE</span><span class="token operator">=</span><span class="token keyword">InnoDB</span><span class="token punctuation">;</span>
</code></pre></div>
<p>Run this manually if you just have a bunch of tables to convert. If you have hundreds or thousands of tables to convert, read on.</p>
<h2 id="requirements"><a href="#requirements" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Requirements</h2>
<p>In my case, I had to convert over 10'000 MyISAM tables (some of which had several GB of data) on multiple MySQL servers. So I sat down and wrote a script that did exactly what I needed with those requirements:</p>
<ul>
<li><strong>Loop over all MyISAM tables</strong> (only user dbs, no system table schemas!)</li>
<li><strong>Limit number of databases</strong> (no matter how many tables they contain) to convert, so that we could split up conversion and spread the migration over several days to limit risk</li>
<li>Could be directly written in <a href="https://dev.mysql.com/doc/mysql-shell/8.0/en/" rel="nofollow noopener noreferrer" target="_blank">MySQL Shell</a> but preferably written in <strong>Python</strong></li>
<li><strong>Logging</strong>: timestamps (start/end of conversion of each table), db name, table name</li>
<li><strong>Dumps whole database as backup</strong> prior to doing conversion.</li>
<li><strong>Conversion should be done directly</strong> with <code>ALTER TABLE ... ENGINE=InnoDB</code> and not by dumping / loading full DB. (uptime!)</li>
<li>Should <strong>break on any failure</strong> (not continuing with next table conversion!) and report to STDOUT</li>
<li>In case of a failure, it should restore data from <strong>DB backup dump</strong></li>
</ul>
<h2 id="myisam-to-innodb-converter"><a href="#myisam-to-innodb-converter" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>MyISAM-to-InnoDB Converter</h2>
<p>I have published this project as it helped me to migrate all customer tables from MyISAM to InnoDB with zero downtime and with near to zero risk:</p>
<ul>
<li><strong><a href="https://gitlab.com/onlime/myisam-to-innodb" rel="nofollow noopener noreferrer" target="_blank">MyISAM-to-InnoDB Converter <code>onlime/myisam-to-innodb</code></a></strong></li>
</ul>
<p>Install this project using Python 3 venv:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">git</span> clone git@gitlab.com:onlime/myisam-to-innodb.git
$ <span class="token builtin class-name">cd</span> myisam-to-innodbd
$ python3 <span class="token parameter variable">-m</span> venv venv
$ <span class="token builtin class-name">.</span> venv/bin/activate
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ pip <span class="token function">install</span> <span class="token parameter variable">-r</span> requirements.txt
</code></pre></div>
<p>MyISAM-to-InnoDB consists of a single script <code>convert.py</code> which is pretty powerful, check <code>convert.py --help</code> for help:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>usage: convert.py [-h] [--dbpattern DBPATTERN] [--skip-dbs SKIP_DBS] [--limit LIMIT] [--not-before NOT_BEFORE]
                  [--login-path LOGIN_PATH] [--backup-dir BACKUP_DIR] [--dryrun] [--logdir LOGDIR] [--verbose] [--debug]

optional arguments:
  -h, --help            show this help message and exit
  --dbpattern DBPATTERN
                        database pattern to limit search
  --skip-dbs SKIP_DBS   databases to ignore (comma separated)
  --limit LIMIT         Limit number of databases (Default: 0 / unlimited)
  --not-before NOT_BEFORE
                        Limit tables by age (update_time)
  --login-path LOGIN_PATH
                        login_path for authentication (Default: client)
  --backup-dir BACKUP_DIR
                        Path for mysqldump backups (Default: backups)
  --dryrun, --dry-run   Don't convert anything, just output MyISAM tables
  --logdir LOGDIR       Logdir path (Default: logs)
  --verbose             Also print all log entries
  --debug               Enable debug mode
</code></pre></div>
<p>Usage examples:</p>
<blockquote>
<p><strong>WARNING:</strong> Do not try this at home! I have added <code>--dryrun</code> to every command, so that it won't touch your existing data. Once you are sure what the command does, run it without the <code>--dryrun</code> flag.</p>
</blockquote>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token builtin class-name">cd</span> <span class="token operator">&#x3C;</span>PROJECT_DIR<span class="token operator">></span>
$ <span class="token builtin class-name">.</span> venv/bin/activate

<span class="token comment"># Convert all user tables</span>
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ ./convert.py <span class="token parameter variable">--dryrun</span>

<span class="token comment"># Convert all tables of a single database</span>
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ ./convert.py <span class="token parameter variable">--dbpattern</span><span class="token operator">=</span>my_db1 <span class="token parameter variable">--dryrun</span>

<span class="token comment"># Convert all tables of a single database</span>
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ ./convert.py <span class="token parameter variable">--dbpattern</span><span class="token operator">=</span>dbname <span class="token parameter variable">--dryrun</span>

<span class="token comment"># Convert all tables of all databases by pattern</span>
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ ./convert.py <span class="token parameter variable">--dbpattern</span><span class="token operator">=</span><span class="token string">'my_db%'</span> <span class="token parameter variable">--dryrun</span>

<span class="token comment"># Convert all tables of all databases by pattern and write backups to some external dir</span>
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ ./convert.py <span class="token parameter variable">--dbpattern</span><span class="token operator">=</span><span class="token string">'my_db%'</span> --backup-dir<span class="token operator">=</span>/backups/myisam-dumps <span class="token parameter variable">--dryrun</span>

<span class="token comment"># Convert all tables of all databases but exclude some single dbs (comma separated list)</span>
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ ./convert.py --skip-dbs<span class="token operator">=</span><span class="token string">'my_db1,my_db2'</span> <span class="token parameter variable">--dryrun</span>
<span class="token comment"># or even combine such excludes with a db pattern</span>
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ ./convert.py <span class="token parameter variable">--dbpattern</span><span class="token operator">=</span><span class="token string">'my_db%'</span> --skip-dbs<span class="token operator">=</span><span class="token string">'my_db1,my_db2'</span> <span class="token parameter variable">--dryrun</span>

<span class="token comment"># Convert all tables and output loglines (in addition to application.log)</span>
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ ./convert.py <span class="token parameter variable">--dryrun</span> <span class="token parameter variable">--verbose</span>

<span class="token comment"># Convert all tables, limited by the next 10 databases with MyISAM tables</span>
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ ./convert.py <span class="token parameter variable">--limit</span><span class="token operator">=</span><span class="token number">10</span> <span class="token parameter variable">--dryrun</span> <span class="token parameter variable">--verbose</span>

<span class="token comment"># Convert all tables, limited by the next 10 databases with MyISAM tables, </span>
<span class="token comment"># also reporting debug messages (e.g. creation of single table backup dumps) on STDOUT</span>
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ ./convert.py <span class="token parameter variable">--limit</span><span class="token operator">=</span><span class="token number">10</span> <span class="token parameter variable">--dryrun</span> <span class="token parameter variable">--verbose</span> <span class="token parameter variable">--debug</span>

<span class="token comment"># Finally, you can limit MyISAM table lookup by age. As we cannot count on create_time, we</span>
<span class="token comment"># only limit by update_time from information_schema.TABLES</span>
<span class="token comment"># This is only used for a very special use case if you want to exclude really old MyISAM </span>
<span class="token comment"># tables which might have been created/used prior a MySQL 5.7 upgrade, avoiding problems like:</span>
<span class="token comment"># https://bugs.mysql.com/bug.php?id=99791</span>
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span>$ ./convert.py --not-before<span class="token operator">=</span><span class="token string">'2018-01-01'</span> <span class="token parameter variable">--dryrun</span> <span class="token parameter variable">--verbose</span>
</code></pre></div>
<p>Or run it in a one-liner (without entering the venv):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token builtin class-name">cd</span> <span class="token operator">&#x3C;</span>PROJECT_DIR<span class="token operator">></span> <span class="token operator">&#x26;&#x26;</span> venv/bin/python convert.py <span class="token parameter variable">--dryrun</span>
</code></pre></div>
<p>If you did not specify <code>--verbose</code>, you might want to monitor the log:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">tail</span> <span class="token parameter variable">-f</span> logs/application.log
</code></pre></div>
<h2 id="run-it-in-a-cronjob"><a href="#run-it-in-a-cronjob" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Run it in a cronjob</h2>
<p>If running this in a cronjob, ensure the following:</p>
<ul>
<li>The user needs to have access to the right <code>--login-path</code> which is stored in his <code>~/.mylogin.cnf</code>. Usually this will be <code>root</code>, as we don't want to store MySQL root credentials in any regular system user environment.</li>
<li>The <code>convert.py</code> script needs to run inside your project's venv</li>
</ul>
<p>example of a nightly cronjob at 04:15AM, only converting MyISAM tables of 10 databases:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token number">15</span> <span class="token number">4</span>    * * *   <span class="token builtin class-name">cd</span> PROJECT_DIR <span class="token operator">&#x26;&#x26;</span> venv/bin/python convert.py <span class="token parameter variable">--limit</span><span class="token operator">=</span><span class="token number">10</span> --backup-dir<span class="token operator">=</span>/backups/myisam-dumps
</code></pre></div>
<h2 id="disable-myisam"><a href="#disable-myisam" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Disable MyISAM</h2>
<p>After having successfully converted all MyISAM tables to InnoDB (check with above queries), it's time to disable MyISAM for good!</p>
<p>Put those storage engines into <a href="https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html#sysvar_disabled_storage_engines" rel="nofollow noopener noreferrer" target="_blank">disabled_storage_engines</a> in your MySQL configuration (<code>/etc/mysql/my.cnf</code> or <code>/etc/mysql/mysql.conf.d/mysqld.cnf </code> on a Debian Linux / <a href="https://www.percona.com/software/mysql-database/percona-server" rel="nofollow noopener noreferrer" target="_blank">Percona Server for MySQL</a>):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-ini"><code><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">mysqld</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">disabled_storage_engines</span> <span class="token punctuation">=</span> <span class="token value attr-value">"<span class="token inner-value">MyISAM,MERGE</span>"</span>
</code></pre></div>
<p>Restart MySQL and say goodbye to MyISAM forever!</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="mysql" term="mysql"/>
        <category label="python" term="python"/>
        <category label="sysadmin" term="sysadmin"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Automated Bayesian Spam/Ham Training with Rspamd]]></title>
        <id>https://pipo.blog/articles/20210311-rspamd-trainer-bayes</id>
        <link href="https://pipo.blog/articles/20210311-rspamd-trainer-bayes"/>
        <updated>2020-03-11T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Automated Bayes Spam/Ham training with Rspamd and Dovecot / IMAPSieve.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/rspamd.png"></p>
                      <p>At <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime GmbH</a> we have migrated the mail infrastructure in Dec 2019 from good old <a href="https://spamassassin.apache.org/" rel="nofollow noopener noreferrer" target="_blank">Spamassassin</a> to <a href="https://rspamd.com/" rel="nofollow noopener noreferrer" target="_blank">Rspamd</a> which greatly improved spam filtering. Rspamd offers a nice way of Bayesian learning in <a href="https://rspamd.com/doc/configuration/statistic.html" rel="nofollow noopener noreferrer" target="_blank">Rspamd statistical module</a>. You can feed emails through <code>rspamc learn_spam</code> or <code>rspamc learn_ham</code> for manual spam/ham training to improve Bayes hit rate.</p>
<p>In the past, we have only internally used Bayesian training. Wouldn't it be nice to let all customers help us improve the Bayes filter / hit rate? Without even asking them to do so?</p>
<!--more-->
<p>In this article, I am going to explain how to set up automated spam/ham Bayes learning for your mail infrastructure, containing the following components:</p>
<ul>
<li><a href="https://dovecot.org/" rel="nofollow noopener noreferrer" target="_blank">Dovecot IMAP server</a> with <a href="https://doc.dovecot.org/configuration_manual/sieve/plugins/imapsieve/" rel="nofollow noopener noreferrer" target="_blank">Pigeonhole: IMAPSieve Plugin</a></li>
<li>A mail account <a href="mailto:spam@example.com">spam@example.com</a> for spam/ham learning</li>
<li>Global Sieve scripts on IMAP server that feed spam/ham into that mail account</li>
<li><a href="https://rspamd.com/" rel="nofollow noopener noreferrer" target="_blank">Rspamd</a> with enabled <a href="https://rspamd.com/doc/configuration/statistic.html" rel="nofollow noopener noreferrer" target="_blank">statistics module</a> (Bayes)</li>
<li><a href="https://gitlab.com/onlime/rspamd-trainer" rel="nofollow noopener noreferrer" target="_blank">onlime/rspamd-trainer</a> doing the actual spam/ham learning with <code>rspamc learn_{spam|ham}</code></li>
</ul>
<p>Further down, I explain the "magic", how our customers are helping improve our Bayes filtering without even noticing.</p>
<h2 id="set-up-rspamd-trainer"><a href="#set-up-rspamd-trainer" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Set up rspamd-trainer</h2>
<p>I have written a small helper script in Python that grabs messages from a mailbox via IMAP and feeds them to Rspamd for spam/ham learning:</p>
<ul>
<li><strong><a href="https://gitlab.com/onlime/rspamd-trainer" rel="nofollow noopener noreferrer" target="_blank">onlime/rspamd-trainer</a></strong></li>
</ul>
<p>First, create a mailaccount for spam learning (using <a href="mailto:spam@example.com">spam@example.com</a> as reference in this article) with at least the following folders:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>INBOX
├── report_ham
├── report_spam
└── report_spam_reply
</code></pre></div>
<p>Additional <code>INBOX/learned_*</code> folders will be created by rspamd-trainer upon first moved emails, if they don't exist. rspamd-trainer grabs emails from <code>report_*</code> folders and moves them to <code>learned_*</code> folders once successfully processed.</p>
<p>Now, install <a href="https://gitlab.com/onlime/rspamd-trainer" rel="nofollow noopener noreferrer" target="_blank">onlime/rspamd-trainer</a> on the same mailserver where Rspamd is running (does not need to run on your IMAP server):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token builtin class-name">cd</span> /opt
$ <span class="token function">git</span> clone git@gitlab.com:onlime/rspamd-trainer.git
$ <span class="token builtin class-name">cd</span> rspamd-trainer
$ python3 <span class="token parameter variable">-m</span> venv venv
$ <span class="token builtin class-name">.</span> venv/bin/activate
<span class="token punctuation">(</span>venv<span class="token punctuation">)</span> $ pip <span class="token function">install</span> <span class="token parameter variable">-r</span> requirements.txt
</code></pre></div>
<p>Configuration is stored in <code>.env</code>. See <code>.env.defaults</code> for default config options. Put the credentials for <a href="mailto:spam@example.com">spam@example.com</a> mailaccount into that file:</p>
<div class="nuxt-content-highlight"><span class="filename">.env</span><pre class="line-numbers language-ini"><code><span class="token key attr-name">HOST</span><span class="token punctuation">=</span><span class="token value attr-value">localhost</span>
<span class="token key attr-name">USERNAME</span><span class="token punctuation">=</span><span class="token value attr-value">spam@example.com</span>
<span class="token key attr-name">PASSWORD</span><span class="token punctuation">=</span><span class="token value attr-value">xxxxxxxxxxxxxxxx</span>
<span class="token key attr-name">INBOXPREFIX</span><span class="token punctuation">=</span><span class="token value attr-value">INBOX/</span>
</code></pre></div>
<p>rspamd-trainer is now ready for work and you can already set up a cronjob that runs e.g. every 5mins:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>*/5 * * * * /opt/rspamd-trainer/venv/bin/python run.py
</code></pre></div>
<p>Now, copy a spam mail that has not been classified as spam by Rspamd into <code>INBOX/report_spam</code> on <a href="mailto:spam@example.com">spam@example.com</a> and monitor rspamd-trainer's log:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">tail</span> <span class="token parameter variable">-f</span> log/application.log
</code></pre></div>
<p>Sample log lines:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>2020-03-11 11:55:01,872 INFO (4e8728ad) - INBOX/report_spam:40484 From:"Badcompany GmbH" &#x3C;spammer@example.com> To:&#x3C;contact@example.com> Message-ID: &#x3C;ff093c4a...>
2020-03-11 11:55:01,873 INFO (4e8728ad) - running rspamc learn_spam ...
2020-03-11 11:55:01,965 INFO (4e8728ad) - rspamc output:
Results for file: stdin (0.035 seconds)
success = true;
filename = "stdin";
scan_time = 0.035999;
</code></pre></div>
<p>Great, <a href="https://gitlab.com/onlime/rspamd-trainer" rel="nofollow noopener noreferrer" target="_blank">rspamd-trainer</a> does its job!</p>
<h2 id="automated-bayes-learning"><a href="#automated-bayes-learning" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Automated Bayes Learning</h2>
<blockquote>
<p>So far, we did not depend on any specific IMAP mailserver implementation, as <a href="https://gitlab.com/onlime/rspamd-trainer" rel="nofollow noopener noreferrer" target="_blank">rspamd-trainer</a> simply connects over IMAP to connect to our spam mailbox for Bayes learning. Now, it gets more vendor specific and I am only going to present you a solution for <a href="https://dovecot.org/" rel="nofollow noopener noreferrer" target="_blank">Dovecot</a>, as Dovecot simply is the best open source IMAP server out there - glad we finally ditched Cyrus-IMAPd in 2019!</p>
</blockquote>
<p>For <strong>automated spam/ham learning</strong> via Dovecot/IMAPSieve, first study the following tutorial for a quick overview:</p>
<ul>
<li><a href="https://rspamd.com/doc/tutorials/feedback_from_users_with_IMAPSieve.html" rel="nofollow noopener noreferrer" target="_blank">Rspamd: Getting feedback from users with IMAPSieve</a></li>
</ul>
<p><strong>The main idea:</strong> Whenever a customer/ mailaccount user is moving an email into his <code>Spam</code> folder, we assume this was an email which was not detected as spam and should be learned as "spam". Whenever he moves an email from his <code>Spam</code> folder into any other folder (other than <code>Trash</code>), we assume this was a false-positive and should be learned as "ham".</p>
<p>Below implementation with global Sieve scripts copies such emails to our spam learning mailbox (in <a href="mailto:spam@example.com">spam@example.com</a> mailaccount we have set up, see above):</p>
<ul>
<li>Copy an email to the <code>report_spam</code> mailbox if a user copies it from elsewhere to his <code>Spam</code> folder or if a flag is changed on an email in <code>Spam</code> folder.</li>
<li>Copy an email to the <code>report_ham</code> mailbox if a user copies it from his <code>Spam</code> folder to elsewhere.</li>
</ul>
<p>Spam/Ham learning is triggered via Dovecot/IMAPSieve configuration in <code>conf.d/90-sieve.conf</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">conf.d/90-sieve.conf</span><pre class="line-numbers language-ini"><code>plugin {
  <span class="token comment"># ...</span>

  <span class="token comment">###</span>
  <span class="token comment">### Spam learning with IMAPSieve</span>
  <span class="token comment">### Note: MUAs may move message with COPY or APPEND (MS Outlook) (IMAP) command.</span>
  <span class="token comment">###</span>
  <span class="token comment"># Spam: From elsewhere to Spam folder or flag changed in Spam folder</span>
  <span class="token key attr-name">imapsieve_mailbox1_name</span> <span class="token punctuation">=</span> <span class="token value attr-value">INBOX/Spam</span>
  <span class="token key attr-name">imapsieve_mailbox1_causes</span> <span class="token punctuation">=</span> <span class="token value attr-value">COPY APPEND FLAG</span>
  <span class="token key attr-name">imapsieve_mailbox1_before</span> <span class="token punctuation">=</span> <span class="token value attr-value">file:/var/lib/dovecot/sieve/learn-spam.sieve</span>

  <span class="token comment"># Ham: From Spam folder to elsewhere</span>
  <span class="token key attr-name">imapsieve_mailbox2_name</span> <span class="token punctuation">=</span> <span class="token value attr-value">*</span>
  <span class="token key attr-name">imapsieve_mailbox2_from</span> <span class="token punctuation">=</span> <span class="token value attr-value">INBOX/Spam</span>
  <span class="token key attr-name">imapsieve_mailbox2_causes</span> <span class="token punctuation">=</span> <span class="token value attr-value">COPY</span>
  <span class="token key attr-name">imapsieve_mailbox2_before</span> <span class="token punctuation">=</span> <span class="token value attr-value">file:/var/lib/dovecot/sieve/learn-ham.sieve</span>

  <span class="token comment"># ...</span>
}
</code></pre></div>
<p>Global <code>learn-spam.sieve</code> that cares about <strong>spam learning</strong> and writes log lines with <code>learn-spam</code> keyword:</p>
<div class="nuxt-content-highlight"><span class="filename">learn-spam.sieve</span><pre class="line-numbers language-ruby"><code><span class="token keyword">require</span> <span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"vnd.dovecot.pipe"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"copy"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"imapsieve"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"environment"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"imap4flags"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"vnd.dovecot.debug"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"variables"</span></span><span class="token punctuation">]</span><span class="token punctuation">;</span>

<span class="token comment"># Logging</span>
<span class="token keyword">if</span> address <span class="token symbol">:matches</span> <span class="token string-literal"><span class="token string">"from"</span></span> <span class="token string-literal"><span class="token string">"*"</span></span> <span class="token punctuation">{</span> set <span class="token string-literal"><span class="token string">"FROM"</span></span> <span class="token string-literal"><span class="token string">"${1}"</span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">if</span> address <span class="token symbol">:matches</span> <span class="token string-literal"><span class="token string">"to"</span></span> <span class="token string-literal"><span class="token string">"*"</span></span> <span class="token punctuation">{</span> set <span class="token string-literal"><span class="token string">"TO"</span></span> <span class="token string-literal"><span class="token string">"${1}"</span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">if</span> header <span class="token symbol">:matches</span> <span class="token string-literal"><span class="token string">"subject"</span></span> <span class="token string-literal"><span class="token string">"*"</span></span> <span class="token punctuation">{</span> set <span class="token string-literal"><span class="token string">"SUBJECT"</span></span> <span class="token string-literal"><span class="token string">"${1}"</span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">if</span> header <span class="token symbol">:matches</span> <span class="token string-literal"><span class="token string">"Message-ID"</span></span> <span class="token string-literal"><span class="token string">"*"</span></span> <span class="token punctuation">{</span> set <span class="token string-literal"><span class="token string">"MSGID"</span></span> <span class="token string-literal"><span class="token string">"${1}"</span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">if</span> header <span class="token symbol">:matches</span> <span class="token string-literal"><span class="token string">"X-Spamd-Result"</span></span> <span class="token string-literal"><span class="token string">"*"</span></span> <span class="token punctuation">{</span> set <span class="token string-literal"><span class="token string">"XSpamdResult"</span></span> <span class="token string-literal"><span class="token string">"${1}"</span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">if</span> environment <span class="token symbol">:matches</span> <span class="token string-literal"><span class="token string">"imap.cause"</span></span> <span class="token string-literal"><span class="token string">"*"</span></span> <span class="token punctuation">{</span> set <span class="token string-literal"><span class="token string">"IMAPCAUSE"</span></span> <span class="token string-literal"><span class="token string">"${1}"</span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
debug_log <span class="token string-literal"><span class="token string">"learn-spam.sieve was triggered on imap.cause=${IMAPCAUSE}: msgid=${MSGID}"</span></span><span class="token punctuation">;</span>
set <span class="token string-literal"><span class="token string">"LogMsg"</span></span> <span class="token string-literal"><span class="token string">"learn-spam on imap.cause=${IMAPCAUSE}: from=${FROM}, to=${TO}, subject=${SUBJECT}, msgid=${MSGID}, X-Spamd-Result=${XSpamdResult}"</span></span><span class="token punctuation">;</span>

<span class="token comment"># Spam-learning by storing a copy of the message into spam@example.com</span>
<span class="token keyword">if</span> anyof <span class="token punctuation">(</span>environment <span class="token symbol">:is</span> <span class="token string-literal"><span class="token string">"imap.cause"</span></span> <span class="token string-literal"><span class="token string">"COPY"</span></span><span class="token punctuation">,</span> environment <span class="token symbol">:is</span> <span class="token string-literal"><span class="token string">"imap.cause"</span></span> <span class="token string-literal"><span class="token string">"APPEND"</span></span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    debug_log <span class="token string-literal"><span class="token string">"${LogMsg}"</span></span><span class="token punctuation">;</span>
    debug_log <span class="token string-literal"><span class="token string">"learn-spam copy to INBOX/report_spam"</span></span><span class="token punctuation">;</span>
    pipe <span class="token symbol">:copy</span> <span class="token string-literal"><span class="token string">"dovecot-lda"</span></span> <span class="token punctuation">[</span> <span class="token string-literal"><span class="token string">"-d"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"spam@example.com"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"-m"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"INBOX/report_spam"</span></span> <span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token comment"># Catch replied or forwarded spam</span>
<span class="token keyword">elsif</span> anyof <span class="token punctuation">(</span>allof <span class="token punctuation">(</span>hasflag <span class="token string-literal"><span class="token string">"\\Answered"</span></span><span class="token punctuation">,</span> environment <span class="token symbol">:contains</span> <span class="token string-literal"><span class="token string">"imap.changedflags"</span></span> <span class="token string-literal"><span class="token string">"\\Answered"</span></span><span class="token punctuation">)</span><span class="token punctuation">,</span>
             allof <span class="token punctuation">(</span>hasflag <span class="token string-literal"><span class="token string">"$Forwarded"</span></span><span class="token punctuation">,</span> environment <span class="token symbol">:contains</span> <span class="token string-literal"><span class="token string">"imap.changedflags"</span></span> <span class="token string-literal"><span class="token string">"$Forwarded"</span></span><span class="token punctuation">)</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    debug_log <span class="token string-literal"><span class="token string">"${LogMsg}"</span></span><span class="token punctuation">;</span>
    debug_log <span class="token string-literal"><span class="token string">"learn-spam copy to INBOX/report_spam_reply"</span></span><span class="token punctuation">;</span>
    pipe <span class="token symbol">:copy</span> <span class="token string-literal"><span class="token string">"dovecot-lda"</span></span> <span class="token punctuation">[</span> <span class="token string-literal"><span class="token string">"-d"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"spam@example.com"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"-m"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"INBOX/report_spam_reply"</span></span> <span class="token punctuation">]</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Global <code>learn-ham.sieve</code> that cares about <strong>ham learning</strong> and writes log lines with <code>learn-ham</code> keyword:</p>
<div class="nuxt-content-highlight"><span class="filename">learn-ham.sieve</span><pre class="line-numbers language-ruby"><code><span class="token keyword">require</span> <span class="token punctuation">[</span><span class="token string-literal"><span class="token string">"vnd.dovecot.pipe"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"copy"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"imapsieve"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"environment"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"variables"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"vnd.dovecot.debug"</span></span><span class="token punctuation">]</span><span class="token punctuation">;</span>

<span class="token comment"># Exclude messages which were moved to Trash (or training mailboxes) from ham learning</span>
<span class="token keyword">if</span> environment <span class="token symbol">:matches</span> <span class="token string-literal"><span class="token string">"imap.mailbox"</span></span> <span class="token string-literal"><span class="token string">"*"</span></span> <span class="token punctuation">{</span>
    set <span class="token string-literal"><span class="token string">"mailbox"</span></span> <span class="token string-literal"><span class="token string">"${1}"</span></span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>
<span class="token keyword">if</span> string <span class="token string-literal"><span class="token string">"${mailbox}"</span></span> <span class="token punctuation">[</span> <span class="token string-literal"><span class="token string">"INBOX/Trash"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"INBOX/Deleted Items"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"INBOX/Bin"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"INBOX/train_ham"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"INBOX/train_prob"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"INBOX/train_spam"</span></span> <span class="token punctuation">]</span> <span class="token punctuation">{</span>
    stop<span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token comment"># Logging</span>
<span class="token keyword">if</span> address <span class="token symbol">:matches</span> <span class="token string-literal"><span class="token string">"from"</span></span> <span class="token string-literal"><span class="token string">"*"</span></span> <span class="token punctuation">{</span> set <span class="token string-literal"><span class="token string">"FROM"</span></span> <span class="token string-literal"><span class="token string">"${1}"</span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">if</span> address <span class="token symbol">:matches</span> <span class="token string-literal"><span class="token string">"to"</span></span> <span class="token string-literal"><span class="token string">"*"</span></span> <span class="token punctuation">{</span> set <span class="token string-literal"><span class="token string">"TO"</span></span> <span class="token string-literal"><span class="token string">"${1}"</span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">if</span> header <span class="token symbol">:matches</span> <span class="token string-literal"><span class="token string">"subject"</span></span> <span class="token string-literal"><span class="token string">"*"</span></span> <span class="token punctuation">{</span> set <span class="token string-literal"><span class="token string">"SUBJECT"</span></span> <span class="token string-literal"><span class="token string">"${1}"</span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">if</span> header <span class="token symbol">:matches</span> <span class="token string-literal"><span class="token string">"Message-ID"</span></span> <span class="token string-literal"><span class="token string">"*"</span></span> <span class="token punctuation">{</span> set <span class="token string-literal"><span class="token string">"MSGID"</span></span> <span class="token string-literal"><span class="token string">"${1}"</span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">if</span> header <span class="token symbol">:matches</span> <span class="token string-literal"><span class="token string">"X-Spamd-Result"</span></span> <span class="token string-literal"><span class="token string">"*"</span></span> <span class="token punctuation">{</span> set <span class="token string-literal"><span class="token string">"XSpamdResult"</span></span> <span class="token string-literal"><span class="token string">"${1}"</span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token keyword">if</span> environment <span class="token symbol">:matches</span> <span class="token string-literal"><span class="token string">"imap.cause"</span></span> <span class="token string-literal"><span class="token string">"*"</span></span> <span class="token punctuation">{</span> set <span class="token string-literal"><span class="token string">"IMAPCAUSE"</span></span> <span class="token string-literal"><span class="token string">"${1}"</span></span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
debug_log <span class="token string-literal"><span class="token string">"learn-ham on imap.cause=${IMAPCAUSE}: from=${FROM}, to=${TO}, subject=${SUBJECT}, msgid=${MSGID}, X-Spamd-Result=${XSpamdResult}"</span></span><span class="token punctuation">;</span>

<span class="token comment"># Ham-learning by storing a copy of the message into spam@example.com</span>
debug_log <span class="token string-literal"><span class="token string">"learn-ham copy to INBOX/report_ham"</span></span><span class="token punctuation">;</span>
pipe <span class="token symbol">:copy</span> <span class="token string-literal"><span class="token string">"dovecot-lda"</span></span> <span class="token punctuation">[</span> <span class="token string-literal"><span class="token string">"-d"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"spam@example.com"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"-m"</span></span><span class="token punctuation">,</span> <span class="token string-literal"><span class="token string">"INBOX/report_ham"</span></span> <span class="token punctuation">]</span><span class="token punctuation">;</span>
</code></pre></div>
<p>Prepare Dovecot and compile global Sieve scripts:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">ln</span> <span class="token parameter variable">-s</span> /usr/lib/dovecot/dovecot-lda /usr/local/sbin/dovecot-lda
$ sievec /var/lib/dovecot/sieve/learn-spam.sieve
$ sievec /var/lib/dovecot/sieve/learn-ham.sieve
</code></pre></div>
<p>Once this is all set up, monitor <code>mail.log</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">tail</span> <span class="token parameter variable">-f</span> /var/log/mail.log <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">-E</span> <span class="token string">'sieve.*learn'</span>
</code></pre></div>
<p>Emails are now copied from all mailaccounts to the spam/ham learning mailbox on <a href="mailto:spam@example.com">spam@example.com</a> and <a href="https://gitlab.com/onlime/rspamd-trainer" rel="nofollow noopener noreferrer" target="_blank">rspamd-trainer</a> feeds it to Rspamd for Bayesian learning. If ever you want to reverse learning, simply move the (wrongly learned) emails from any <code>learned_*</code> folder to <code>report_*</code> folder.</p>
<h2 id="maintenance"><a href="#maintenance" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Maintenance</h2>
<p>Make sure you set quota for the global mailaccount <a href="mailto:spam@example.com">spam@example.com</a> high enough, so that you never run out of storage space. Also make sure you protect that account with a strong password and never hand those credentials out to anybody else than the sysadmin of your mail infrastruture (geeky you, as probably you wouldn't have read this article if it was somebody else), as this mailaccount may contain sensible emails from your friends/customers.</p>
<p>And, once in a while, do some cleanup and e.g. remove any learned messages that are older than 90 days, using <code>doveadm</code> on your Dovecot mailserver:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token comment"># check number of messages and mailbox size of INBOX/learned_spam</span>
$ doveadm mailbox status <span class="token parameter variable">-u</span> spam@example.com <span class="token string">'messages vsize'</span> INBOX/learned_spam
<span class="token comment"># remove any emails older than 90d from INBOX/learned_spam</span>
$ doveadm expunge <span class="token parameter variable">-u</span> spam@example.com mailbox INBOX/learned_spam BEFORE 90d <span class="token operator">></span> /dev/null <span class="token operator"><span class="token file-descriptor important">2</span>></span><span class="token file-descriptor important">&#x26;1</span>

<span class="token comment"># check number of messages and mailbox size of INBOX/learned_ham</span>
$ doveadm mailbox status <span class="token parameter variable">-u</span> spam@example.com <span class="token string">'messages vsize'</span> INBOX/learned_ham
<span class="token comment"># remove any emails older than 90d from INBOX/learned_ham</span>
$ doveadm expunge <span class="token parameter variable">-u</span> spam@example.com mailbox INBOX/learned_ham BEFORE 90d <span class="token operator">></span> /dev/null <span class="token operator"><span class="token file-descriptor important">2</span>></span><span class="token file-descriptor important">&#x26;1</span>
</code></pre></div>
<p>You might automate this cleanup task with a script that you run in a cronjob or Systemd timer.</p>
<p>Happy learning and never stop fighting spam!</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="rspamd" term="rspamd"/>
        <category label="dovecot" term="dovecot"/>
        <category label="python" term="python"/>
        <category label="email" term="email"/>
        <category label="sysadmin" term="sysadmin"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Process hiding in LXC using hidepid capabilities of procfs]]></title>
        <id>https://pipo.blog/articles/20180806-lxc-hidepid-process-hiding</id>
        <link href="https://pipo.blog/articles/20180806-lxc-hidepid-process-hiding"/>
        <updated>2018-08-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Hiding processes for other users using hidepid capabilities of procfs in an LXC container.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/lxc-apparmor.jpg"></p>
                      <p>Back in 2013, I wrote about <a href="/articles/20130930-hidepid-process-hiding">Linux process hiding using hidepid capabilities of procfs</a>. On shared webhosting servers at <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime GmbH</a>, I have used the <code>hidepid=2</code> mount option for procfs (<code>/proc</code> filesystem) for improved security. Like this, a regular system user (which could potentially be an evil customer that has gained SSH access and tries to spy on other's processes) does only see his own processes, all other processes are hidden.</p>
<p>This is great and super simple to enable, as it is part of the official Linux kernel for quite a while now. But things start to get a little trickier when we try to set up <code>hidepid</code> procfs mount option inside an <a href="https://linuxcontainers.org/" rel="nofollow noopener noreferrer" target="_blank">LXC container</a>. Enabling the mount option on the host system will not do! Inside an LXC container, a regular system user is still able to see all processes. Before <strong>LXC 2.1</strong> (released in Sept 2017), this was also quite doable, as we just had to create a new <a href="https://en.wikipedia.org/wiki/AppArmor" rel="nofollow noopener noreferrer" target="_blank">AppArmor</a> profile on the host system to allow the LXC container to set the <code>/proc</code> mount options. But since LXC 2.1 it got super tricky. I will present both solutions below, in case you have struggled with this hard one in newer LXC versions.</p>
<!--more-->
<h2 id="recap-procfs-hidepid-mount-option"><a href="#recap-procfs-hidepid-mount-option" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Recap: procfs hidepid mount option</h2>
<p><code>man proc</code> explains the <code>hidepid</code> capability of procfs in depth:</p>
<div class="nuxt-content-highlight"><span class="filename">PROC(5)</span><pre class="line-numbers language-text"><code>   Mount options
       The proc filesystem supports the following mount options:

       hidepid=n (since Linux 3.3)
              This option controls who can access the information in /proc/[pid] directories.  The  ar‐
              gument, n, is one of the following values:

              0   Everybody  may access all /proc/[pid] directories.  This is the traditional behavior,
                  and the default if this mount option is not specified.

              1   Users may not access files and subdirectories inside any /proc/[pid] directories  but
                  their  own  (the /proc/[pid] directories themselves remain visible).  Sensitive files
                  such as /proc/[pid]/cmdline and /proc/[pid]/status are now  protected  against  other
                  users.  This makes it impossible to learn whether any user is running a specific pro‐
                  gram (so long as the program doesn't otherwise reveal itself by its behavior).

              2   As for mode 1, but in addition the /proc/[pid] directories belonging to  other  users
                  become  invisible.  This means that /proc/[pid] entries can no longer be used to dis‐
                  cover the PIDs on the system.  This doesn't hide the fact that a process with a  spe‐
                  cific  PID  value  exists (it can be learned by other means, for example, by "kill -0
                  $PID"), but it hides a process's UID and GID, which could otherwise be learned by em‐
                  ploying  stat(2)  on a /proc/[pid] directory.  This greatly complicates an attacker's
                  task of gathering information about running processes (e.g., discovering whether some
                  daemon is running with elevated privileges, whether another user is running some sen‐
                  sitive program, whether other users are running any program at all, and so on).
</code></pre></div>
<p>So, <code>hidepid=2</code> is what we want! On a standard Linux (we're using stable Debian Linux everywhere), you can enable this procfs mount option by a simple remount:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">mount</span> <span class="token parameter variable">-o</span> remount,hidepid<span class="token operator">=</span><span class="token number">2</span> /proc
</code></pre></div>
<p>You may also add this mount option directly to <code>/etc/fstab</code> in order to make it persistent over reboots:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>proc            /proc           proc    defaults,hidepid=2        0       0
</code></pre></div>
<p>A regular system user will then only see his own processes, e.g.:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>testuser@web:~$ <span class="token function">ps</span>
  PID TTY          TIME CMD
<span class="token number">17486</span> pts/0    00:00:00 <span class="token function">bash</span>
<span class="token number">24806</span> pts/0    00:00:00 <span class="token function">ps</span>
</code></pre></div>
<p>This also works for other commands like <strong>pstree</strong>, <strong>top</strong>, <strong>htop</strong>,...</p>
<p>All good so far, but this article is about LXC containers with its super strict AppArmor policies that make things a little bit harder...</p>
<h2 id="procfs-mount-options-in-lxc-before-v21"><a href="#procfs-mount-options-in-lxc-before-v21" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Procfs mount options in LXC before v2.1</h2>
<p>On an host system with an LXC version before 2.1, in our case <a href="https://proxmox.com/en/proxmox-ve" rel="nofollow noopener noreferrer" target="_blank">Proxmox VE</a> up until 5.0 (LXC 2.1 was introduced in Proxmox VE 5.1 in Oct 2017), you could allow the container to set procfs mount options by creating a new <a href="https://en.wikipedia.org/wiki/AppArmor" rel="nofollow noopener noreferrer" target="_blank">AppArmor</a> profile. I have described this in the following Proxmox VE forum post:</p>
<ul>
<li><a href="https://forum.proxmox.com/threads/process-hiding-in-lxc-hidepid-proc-mount-option.28935/#post-182915" rel="nofollow noopener noreferrer" target="_blank">Process hiding in LXC (hidepid proc mount option)</a></li>
</ul>
<p>On the host system (running Proxmox VE or any system that offers LXC &#x3C; 2.1), copy the default AppArmor profile <code>/etc/apparmor.d/lxc/lxc-default-cgns</code> to <code>/etc/apparmor.d/lxc/lxc-default-cgns-with-proc-remount</code> with :</p>
<div class="nuxt-content-highlight"><span class="filename">lxc-default-cgns-with-proc-remount</span><pre class="line-numbers language-yaml"><code><span class="token comment"># Do not load this file.  Rather, load /etc/apparmor.d/lxc-containers, which</span>
<span class="token comment"># will source all profiles under /etc/apparmor.d/lxc</span>

profile lxc<span class="token punctuation">-</span>container<span class="token punctuation">-</span>default<span class="token punctuation">-</span>cgns<span class="token punctuation">-</span>with<span class="token punctuation">-</span>proc<span class="token punctuation">-</span>remount flags=(attach_disconnected<span class="token punctuation">,</span>mediate_deleted) <span class="token punctuation">{</span>
  <span class="token comment">#include &#x3C;abstractions/lxc/container-base></span>

  <span class="token comment"># the container may never be allowed to mount devpts.  If it does, it</span>
  <span class="token comment"># will remount the host's devpts.  We could allow it to do it with</span>
  <span class="token comment"># the newinstance option (but, right now, we don't).</span>
  deny mount fstype=devpts<span class="token punctuation">,</span>
  mount fstype=cgroup <span class="token punctuation">-</span><span class="token punctuation">></span> /sys/fs/cgroup/<span class="token important">**</span><span class="token punctuation">,</span>

  <span class="token comment"># This will allow remounting /proc, e.g. to add hidepid=2 mount option</span>
  <span class="token comment"># The hidepid option doesn't need to be included here as it's not known by AppArmor.</span>
  mount options=(rw<span class="token punctuation">,</span>nosuid<span class="token punctuation">,</span>nodev<span class="token punctuation">,</span>noexec<span class="token punctuation">,</span>remount<span class="token punctuation">,</span>relatime) <span class="token punctuation">-</span><span class="token punctuation">></span> /proc/<span class="token punctuation">,</span>
<span class="token punctuation">}</span>
</code></pre></div>
<blockquote>
<p><strong>Note:</strong> We have added the last section, see the diff:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-diff"><code><span class="token coord">--- lxc-default-cgns</span>
<span class="token coord">+++ lxc-default-cgns-with-proc-remount</span>
<span class="token coord">@@ -1,7 +1,7 @@</span>
<span class="token unchanged"><span class="token prefix unchanged"> </span># Do not load this file.  Rather, load /etc/apparmor.d/lxc-containers, which
<span class="token prefix unchanged"> </span># will source all profiles under /etc/apparmor.d/lxc
<span class="token prefix unchanged"> </span>
</span><span class="token deleted-sign deleted"><span class="token prefix deleted">-</span>profile lxc-container-default-cgns flags=(attach_disconnected,mediate_deleted) {
</span><span class="token inserted-sign inserted"><span class="token prefix inserted">+</span>profile lxc-container-default-cgns-with-proc-remount flags=(attach_disconnected,mediate_deleted) {
</span><span class="token unchanged"><span class="token prefix unchanged"> </span>  #include &#x3C;abstractions/lxc/container-base>
<span class="token prefix unchanged"> </span>
<span class="token prefix unchanged"> </span>  # the container may never be allowed to mount devpts.  If it does, it
</span><span class="token coord">@@ -10,4 +10,8 @@</span>
<span class="token unchanged"><span class="token prefix unchanged"> </span>  deny mount fstype=devpts,
<span class="token prefix unchanged"> </span>  mount fstype=cgroup -> /sys/fs/cgroup/**,
<span class="token prefix unchanged"> </span>  mount fstype=cgroup2 -> /sys/fs/cgroup/**,
</span><span class="token inserted-sign inserted"><span class="token prefix inserted">+</span>
<span class="token prefix inserted">+</span>  # This will allow remounting /proc, e.g. to add hidepid=2 mount option
<span class="token prefix inserted">+</span>  # The hidepid option doesn't need to be included here as it's not known by AppArmor.
<span class="token prefix inserted">+</span>  mount options=(rw, nosuid, nodev, noexec, remount, relatime) -> /proc/,
</span><span class="token unchanged"><span class="token prefix unchanged"> </span>}
</span></code></pre></div>
</blockquote>
<p>You can then load the new AppArmor profile:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ apparmor_parser <span class="token parameter variable">-r</span> <span class="token parameter variable">-W</span> <span class="token parameter variable">-T</span> /etc/apparmor.d/lxc-containers
</code></pre></div>
<p>Add the new AppArmor profile to the container config, using the new <strong>lxc.apparmor.profile</strong> key (on a Proxmox VE host, the container config can also be found here: <code>/etc/pve/lxc/$VMID.conf</code>):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ pct config <span class="token variable">$VMID</span> <span class="token operator">|</span> <span class="token function">grep</span> lxc
lxc.apparmor.profile: lxc-container-default-cgns-with-proc-remount
</code></pre></div>
<p>Then, restart the container (again, we're on a Proxmox VE host system, so we use the fancy <code>pct</code> command instead of native low-level lxc commands):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ pct <span class="token function">shutdown</span> <span class="token variable">$VMID</span>
$ pct start <span class="token variable">$VMID</span>
</code></pre></div>
<p>You can now set the procfs mount option in the container's <code>/etc/fstab</code>, as you used to do on a plain Linux system:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>proc  /proc  proc  defaults,noexec,nosuid,nodev,relatime,hidepid=2 0  0
</code></pre></div>
<p>And it all works (check this inside the container):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">mount</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token string">'/proc '</span>
proc on /proc <span class="token builtin class-name">type</span> proc <span class="token punctuation">(</span>rw,nosuid,nodev,noexec,relatime<span class="token punctuation">)</span>
</code></pre></div>
<p>But that will no longer work since <strong>LXC 2.1</strong>, so read on...</p>
<h2 id="procfs-mount-options-in-lxc-21"><a href="#procfs-mount-options-in-lxc-21" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Procfs mount options in LXC 2.1+</h2>
<p>As above solution with a customized AppArmor profile did no longer work since LXC 2.1, I have posted this in LXC forums:</p>
<ul>
<li><a href="https://discuss.linuxcontainers.org/t/hidepid-2-not-working-in-lxc/2060" rel="nofollow noopener noreferrer" target="_blank">Hidepid=2 not working in lxc</a></li>
</ul>
<p>a month later, a smart guy (thanks so much, <a href="https://discuss.linuxcontainers.org/u/brauner" rel="nofollow noopener noreferrer" target="_blank">@brauner</a>!) came up with <a href="https://discuss.linuxcontainers.org/t/hidepid-2-not-working-in-lxc/2060/12" rel="nofollow noopener noreferrer" target="_blank">this workaround</a>. I was super happy about that solution, even though in my eyes it was still a workaround and until now I didn't find a cleaner solution than <a href="https://discuss.linuxcontainers.org/t/hidepid-2-not-working-in-lxc/2060/18" rel="nofollow noopener noreferrer" target="_blank">that</a>.</p>
<p>So, this is the final solution: Add the following lines to your LXC container configuration, e.g. <code>/etc/pve/lxc/$VMID.conf</code> on a Proxmox VE host system):</p>
<div class="nuxt-content-highlight"><span class="filename">/etc/pve/lxc/$VMID.conf</span><pre class="line-numbers language-text"><code># ...
lxc.mount.auto: 
lxc.mount.auto: sys:mixed cgroup:mixed
lxc.mount.entry: proc proc proc rw,remount,nodev,nosuid,noexec,relatime,hidepid=2 0 0
lxc.mount.entry: proc/sys proc/sys proc ro,bind,relative 0 0
lxc.mount.entry: proc/sys/net proc/sys/net proc rw,bind,relative 0 0
lxc.mount.entry: proc/sysrq-trigger proc/sysrq-trigger proc ro,bind,relative 0 0
lxc.mount.entry: /var/lib/lxcfs/proc/cpuinfo proc/cpuinfo none bind,optional 0 0
lxc.mount.entry: /var/lib/lxcfs/proc/diskstats proc/diskstats none bind,optional 0 0
lxc.mount.entry: /var/lib/lxcfs/proc/meminfo proc/meminfo none bind,optional 0 0
lxc.mount.entry: /var/lib/lxcfs/proc/stat proc/stat none bind,optional 0 0
lxc.mount.entry: /var/lib/lxcfs/proc/swaps proc/swaps none bind,optional 0 0
lxc.mount.entry: /var/lib/lxcfs/proc/uptime proc/uptime none bind,optional 0 0
</code></pre></div>
<p>You will no longer need the custom AppArmor profile I have described above. If your LXC config still contains a <code>lxc.apparmor.profile</code> line, remove it!</p>
<p>Once you restart your LXC container with those <code>lxc.mount.*</code> options, you don't even need to add the  <code>hidepid=2</code> mount option to the <code>/etc/fstab</code> inside your container, as this is already done in above LXC configuration.</p>
<blockquote>
<p><strong>UPDATE 2021-08-20:</strong></p>
<p>In latest <strong><a href="https://proxmox.com/en/news/press-releases/proxmox-virtual-environment-7-0" rel="nofollow noopener noreferrer" target="_blank">Proxmox VE 7.0</a> / LXC 4.0.x</strong>, I am still using this solution. Confirmed working. I will keep you posted if I find a simpler solution.</p>
</blockquote>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="lxc" term="lxc"/>
        <category label="security" term="security"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Simple and Secure External Backup]]></title>
        <id>https://pipo.blog/articles/20180306-simple-and-secure-external-backup</id>
        <link href="https://pipo.blog/articles/20180306-simple-and-secure-external-backup"/>
        <updated>2018-03-06T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Howto set up a simple and secure offsite and offline backup server solution based on rsync.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/pexels-photo-117729.jpeg"></p>
                      <p>What we are going to set up here is a <strong>simple and secure offsite and offline backup server</strong>. Let's assume you already have an existing backup server that is connected to the internet 24/7 and does daily/weekly/monthly backups. We would now like to set up a second offsite backup server that just cares about storing data to encrypted external drive and after each backup run, you are going to physically detach that drive.</p>
<p>So, we are talking about offline backups in addition to the fact having this server offsite - at a different location than your main backup server.</p>
<p>Preferably, your main backup server would also be offsite. But as it needs to pull data frequently, its storage is always available and not getting detached.</p>
<p>Let's call your main backup server <code>backup</code> and the one we are going to set up here <code>extbackup</code>.</p>
<!--more-->
<h2 id="requirements"><a href="#requirements" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Requirements</h2>
<p>We have the following – from a security point of view – rather strict requirements:</p>
<ul>
<li>Simple hardware setup: cheap but <strong>reliable fanless system</strong> that could be mounted to any office desk.</li>
<li>System: Standard <strong>Debian Linux</strong></li>
<li><strong>No backup data should ever be stored (not even temporarily!) on our root partition, only on external drives.</strong></li>
<li>System is installed on SSD, external drives attached over USB 3.</li>
<li>External drives fully encrypted using standard encryption. My choice: LUKS</li>
<li><strong>LUKS encryption key</strong> not stored anywhere on SSD, <strong>only generated at runtime</strong>.</li>
<li>Backup data is pulled from <code>backup</code> over SSH.</li>
<li><strong>SSH private key</strong> to connect to <code>backup</code> is not stored anywhere on <code>extbackup</code> in plaintext, <strong>only getting generated at runtime</strong>.</li>
<li><strong>Both LUKS encryption key and SSH private key should not be required to be stored in any password manager or anywhere else.</strong></li>
<li>Unlock password for those keys should only be used as a 2nd factor, so <strong>encryption security does not depend on its length</strong>.</li>
<li>External disks should only get decrypted during a backup run and getting unmounted again right afterwards.</li>
<li>Backup data is directly streamed to external disks without temporary storage on <code>extbackup</code> server.</li>
</ul>
<p>As this tutorial is about a simple yet secure external backup server, I am not going to talk about any snapshot based backup solutions (personally, I love <code>zfs send|receive</code> ...), but using <code>rsync</code> instead.</p>
<h2 id="hardware-setup"><a href="#hardware-setup" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Hardware Setup</h2>
<p>I do recommend the following hardware for a small robust fanless system that is built for 24/7 operation:</p>





















<table><thead><tr><th>component</th><th>spec</th></tr></thead><tbody><tr><td>System</td><td>Shuttle Barebone XPC slim DS77 Series (DS77U3)</td></tr><tr><td>RAM</td><td>SO-DDR4-RAM 2133 MHz 1x 8GB</td></tr><tr><td>SSD</td><td>Samsung SSD 960 EVO NVMe M.2 2280 250GB</td></tr></tbody></table>
<p>This hardware currently (March 2018) is available for $600 in total. You can go much cheaper with some Intel NUC system that comes delivered with a basic RAM / SSD setup. But we do recommend that Shuttle XPC line as it is an industry-grade platform and we never had any issues with its predecessor DS57U.</p>
<p>As external 4TB USB 3.0 drive, I do recommend the following: <strong>Samsung P3 Portable 4TB (HX-MTD40EF/G2)</strong> – Choose any kind of external USB 3.0 drive but make sure it is USB-powered and does not need any extra power cable, so better go for 2.5".</p>
<h2 id="system-setup"><a href="#system-setup" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>System Setup</h2>
<p>Install a standard Debian from <a href="https://www.debian.org/distrib/netinst" rel="nofollow noopener noreferrer" target="_blank">netinst</a>, e.g. via USB stick:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">wget</span> https://cdimage.debian.org/debian-cd/current/amd64/iso-cd/debian-9.3.0-amd64-netinst.iso
$ <span class="token function">cat</span> debian-9.3.0-amd64-netinst.iso <span class="token operator">></span> /dev/disk2
</code></pre></div>
<p>On macOS, use <code>diskutil list</code> to identify the correct device name of your USB stick and run <code>diskutil unmountDisk /dev/disk2</code> prior to writing to it.</p>
<p>I am not going to explain how to set up Debian Linux here as I'm sure you are going to manage that by yourself.</p>
<h2 id="build-encryption-key"><a href="#build-encryption-key" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Build encryption key</h2>
<p>As promised, the LUKS encryption key is only going to be stored at runtime in ramfs (volatile memory aka. RAM). It is built upon first login as root. So let's hook below script into <code>/root/.profile</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token builtin class-name">echo</span> <span class="token string">"/usr/local/sbin/build-encryption-key.sh"</span> <span class="token operator">>></span> /root/.profile
</code></pre></div>
<p>Deploy the following script to <code>/usr/local/sbin/build-encryption-key.sh</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">build-encryption-key.sh</span><pre class="line-numbers language-bash"><code><span class="token shebang important">#!/bin/bash</span>
<span class="token comment">#</span>
<span class="token comment"># This script usually is called on the first login and asks for a password</span>
<span class="token comment"># to build the LUKS encryption key which then is stored only in volatile</span>
<span class="token comment"># memory: /mnt/ramfs/luks_pw (ramfs).</span>
<span class="token comment"># This script is added to your /root/.profile in order you won't forget to</span>
<span class="token comment"># build the encryption key each time you reboot the server.</span>
<span class="token comment">#</span>
<span class="token comment"># We are using ramfs instead of tmpfs as there is no swapping support in </span>
<span class="token comment"># ramfs which is good in a security perspective.</span>
<span class="token comment"># see: http://www.thegeekstuff.com/2008/11/overview-of-ramfs-and-tmpfs-on-linux</span>
<span class="token comment">#</span>
<span class="token comment"># If ever you need to change the password of an LUKS encrypted volume:</span>
<span class="token comment">#</span>
<span class="token comment">#  $ cryptsetup luksChangeKey /dev/sdb1 /mnt/ramfs/luks_pw</span>
<span class="token comment">#  Enter passphrase to be changed: (old password)</span>
<span class="token comment">#</span>


<span class="token comment">################### CONFIGURATION #######################</span>
<span class="token assign-left variable">RAMFS_PATH</span><span class="token operator">=</span>/mnt/ramfs
<span class="token assign-left variable">RAMFS_SIZE</span><span class="token operator">=</span>20M
<span class="token assign-left variable">KEYFILE</span><span class="token operator">=</span><span class="token variable">$RAMFS_PATH</span>/luks_pw
<span class="token assign-left variable">SALT</span><span class="token operator">=</span><span class="token string">'bo1AZ+pX1H%qgIiPDFy74.GB7GvJ6d/dqlV5%bjf861PV-evbC'</span>
<span class="token assign-left variable">SHA1_CHECK</span><span class="token operator">=</span><span class="token string">'fb9e740efe20f541349d37eff7aa34efd4ac823d'</span>
<span class="token comment">#########################################################</span>

<span class="token function-name function">printinfo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"[INFO] <span class="token variable">$1</span>"</span>
<span class="token punctuation">}</span>

<span class="token function-name function">printwarn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"[WARNING] <span class="token variable">$1</span>"</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">--color</span> <span class="token string">"WARNING"</span>
<span class="token punctuation">}</span>

<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token parameter variable">-f</span> <span class="token string">"<span class="token variable">$KEYFILE</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token comment"># exit silently, as the key file already exists</span>
    <span class="token builtin class-name">exit</span>
<span class="token keyword">else</span>
    printwarn <span class="token string">"The LUKS key file (<span class="token variable">$KEYFILE</span>) does not yet exist!"</span>
<span class="token keyword">fi</span>

<span class="token comment"># set up RAM disk if it is not yet mounted</span>
<span class="token keyword">if</span> <span class="token operator">!</span> mountpoint <span class="token parameter variable">-q</span> <span class="token string">"<span class="token variable">$RAMFS_PATH</span>"</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"Setting up ramfs on <span class="token variable">$RAMFS_PATH</span> (size=<span class="token variable">$RAMFS_SIZE</span>) ..."</span>
    <span class="token function">mkdir</span> <span class="token parameter variable">-p</span> <span class="token variable">$RAMFS_PATH</span>
    <span class="token function">mount</span> <span class="token parameter variable">-t</span> ramfs <span class="token parameter variable">-o</span> <span class="token assign-left variable">size</span><span class="token operator">=</span><span class="token variable">$RAMFS_SIZE</span> ramfs <span class="token variable">$RAMFS_PATH</span>
    <span class="token keyword">if</span> <span class="token operator">!</span> <span class="token function">grep</span> <span class="token parameter variable">-q</span> <span class="token string">"<span class="token variable">$RAMFS_PATH</span>"</span> /etc/fstab<span class="token punctuation">;</span> <span class="token keyword">then</span>
        <span class="token builtin class-name">echo</span> <span class="token string">"Adding line to /etc/fstab to persist mounting of <span class="token variable">$RAMFS_PATH</span> ..."</span>
        <span class="token builtin class-name">echo</span> <span class="token string">"ramfs   <span class="token variable">$RAMFS_PATH</span>              ramfs   defaults,size=<span class="token variable">$RAMFS_SIZE</span>        0 0"</span> <span class="token operator">>></span> /etc/fstab
    <span class="token keyword">fi</span>
<span class="token keyword">fi</span>

<span class="token comment"># make this script start on each login</span>
<span class="token assign-left variable">SCRIPT</span><span class="token operator">=</span><span class="token variable"><span class="token variable">$(</span>readlink <span class="token parameter variable">-f</span> $0<span class="token variable">)</span></span>
<span class="token keyword">if</span> <span class="token operator">!</span> <span class="token function">grep</span> <span class="token parameter variable">-q</span> <span class="token string">"<span class="token variable">$SCRIPT</span>"</span> /root/.profile<span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"<span class="token variable">$SCRIPT</span>"</span> <span class="token operator">>></span> /root/.profile
<span class="token keyword">fi</span>

<span class="token comment"># get password from interactive user input</span>
<span class="token keyword">while</span> <span class="token builtin class-name">read</span> <span class="token parameter variable">-s</span> <span class="token parameter variable">-p</span> <span class="token string">'Unlock LUKS encryption key: '</span> PASS <span class="token operator">&#x26;&#x26;</span> <span class="token punctuation">[</span><span class="token punctuation">[</span> <span class="token variable"><span class="token variable">$(</span><span class="token builtin class-name">echo</span> <span class="token parameter variable">-n</span> <span class="token string">"<span class="token variable">$PASS</span>"</span> <span class="token operator">|</span> <span class="token function">wc</span> <span class="token parameter variable">--chars</span><span class="token variable">)</span></span> <span class="token parameter variable">-lt</span> <span class="token number">8</span> <span class="token punctuation">]</span><span class="token punctuation">]</span> <span class="token punctuation">;</span> <span class="token keyword">do</span>
    <span class="token builtin class-name">echo</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"Your password must be at least 8 characters long!"</span>
<span class="token keyword">done</span>
<span class="token builtin class-name">echo</span>

<span class="token comment"># calculate encryption key (SHA-512 hash of salt.password concatenation)</span>
<span class="token assign-left variable">KEY</span><span class="token operator">=</span><span class="token variable"><span class="token variable">`</span><span class="token builtin class-name">echo</span> <span class="token parameter variable">-n</span> <span class="token string">"<span class="token variable">$SALT</span>.<span class="token variable">$PASS</span>"</span> <span class="token operator">|</span> sha512sum <span class="token operator">|</span> <span class="token function">cut</span> -d<span class="token string">' '</span> <span class="token parameter variable">-f1</span><span class="token variable">`</span></span>

<span class="token comment"># store LUKS key file to ramfs</span>
<span class="token function">touch</span> <span class="token variable">$KEYFILE</span> <span class="token operator">&#x26;&#x26;</span> <span class="token function">chmod</span> <span class="token number">600</span> <span class="token variable">$KEYFILE</span>
<span class="token builtin class-name">echo</span> <span class="token parameter variable">-n</span> <span class="token string">"<span class="token variable">$KEY</span>"</span> <span class="token operator">></span> <span class="token variable">$KEYFILE</span>

<span class="token comment"># SHA-1 check of the key - assure you have correctly built it by entering the correct password</span>
<span class="token assign-left variable">KEY_SHA1</span><span class="token operator">=</span><span class="token variable"><span class="token variable">`</span><span class="token function">cat</span> $KEYFILE <span class="token operator">|</span> sha1sum <span class="token operator">|</span> <span class="token function">cut</span> -d<span class="token string">' '</span> <span class="token parameter variable">-f1</span><span class="token variable">`</span></span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token string">"<span class="token variable">$KEY_SHA1</span>"</span> <span class="token operator">!=</span> <span class="token string">"<span class="token variable">$SHA1_CHECK</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    printwarn <span class="token string">"Your key does not seem to be correct. You might have entered the wrong password. Please run <span class="token variable">$SCRIPT</span> again!"</span>
    printinfo <span class="token string">"If you are sure you have entered the right password, try to set SHA1_CHECK='<span class="token variable">$KEY_SHA1</span>' in <span class="token variable"><span class="token variable">`</span><span class="token function">basename</span> $0<span class="token variable">`</span></span>."</span>
    <span class="token function">rm</span> <span class="token parameter variable">-f</span> <span class="token variable">$KEYFILE</span>
    <span class="token builtin class-name">exit</span> <span class="token number">1</span>
<span class="token keyword">fi</span>

printinfo <span class="token string">"The LUKS key file was successfully stored in <span class="token variable">$KEYFILE</span>."</span>


<span class="token comment"># SSH private key encryption</span>
<span class="token comment">#</span>
<span class="token comment"># SETUP:</span>
<span class="token comment"># Initially, we need to create an encrypted version of id_rsa and destroy the plaintext private key:</span>
<span class="token comment">#   $ cat ~/.ssh/id_rsa | openssl enc -e -aes-256-cbc -a -k "$(cat /mnt/ramfs/luks_pw)" > ~/.ssh/id_rsa.encrypted</span>
<span class="token comment"># or better just copy the password from /mnt/ramfs/luks_pw and enter it interactively:</span>
<span class="token comment">#   $ cat ~/.ssh/id_rsa | openssl enc -e -aes-256-cbc -a > ~/.ssh/id_rsa.encrypted</span>
<span class="token comment">#   $ enter aes-256-cbc encryption password: (...)</span>
<span class="token comment">#   $ chmod 600 ~/.ssh/id_rsa.encrypted</span>
<span class="token comment">#   $ shred -u ~/.ssh/id_rsa</span>
<span class="token comment">#</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token parameter variable">-f</span> ~/.ssh/id_rsa.encrypted <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    <span class="token function">touch</span> <span class="token variable">$RAMFS_PATH</span>/id_rsa.decrypted <span class="token operator">&#x26;&#x26;</span> <span class="token function">chmod</span> <span class="token number">600</span> <span class="token variable">$RAMFS_PATH</span>/id_rsa.decrypted
    <span class="token function">cat</span> ~/.ssh/id_rsa.encrypted <span class="token operator">|</span> openssl base64 <span class="token parameter variable">-d</span> <span class="token operator">|</span> openssl enc <span class="token parameter variable">-d</span> -aes-256-cbc <span class="token parameter variable">-k</span> <span class="token string">"<span class="token variable"><span class="token variable">$(</span><span class="token function">cat</span> $KEYFILE<span class="token variable">)</span></span>"</span> <span class="token operator">></span> <span class="token variable">$RAMFS_PATH</span>/id_rsa.decrypted
    <span class="token function">ln</span> <span class="token parameter variable">-sf</span> <span class="token variable">$RAMFS_PATH</span>/id_rsa.decrypted ~/.ssh/id_rsa
    printinfo <span class="token string">"SSH private key was successfully decrypted to <span class="token variable">$RAMFS_PATH</span>/id_rsa.decrypted."</span>
<span class="token keyword">else</span>
    printwarn <span class="token string">"Please encrypt your SSH private key to ~/.ssh/id_rsa.encrypted so I can decrypt it to ramfs."</span>
<span class="token keyword">fi</span>
</code></pre></div>
<p>But before using this script in production, ensure you have set your own <code>SALT</code> in the CONFIGURATION section. Don't yet care about <code>SHA1_CHECK</code> - this is only used to verify that the key got correctly built. Upon first run, the script will provide you with the right information (the unlock password can be freely chosen, just please remember it!):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>$ build-encryption-key.sh
Unlock LUKS encryption key: 
[WARNING] Your key does not seem to be correct. You might have entered the wrong password. Please run build-encryption-key.sh again!
[INFO] If you are sure you have entered the right password, try to set SHA1_CHECK='fb9e740efe20f541349d37eff7aa34efd4ac823d' in build-encryption-key.sh.
</code></pre></div>
<blockquote>
<p>The unlock password is only needed to unlock/generate your LUKS encryption key and is only used as a second factor. For LUKS encryption, we are then only going to use the generated <code>/mnt/ramfs/luks_pw</code> which you should not write down anywhere!</p>
</blockquote>
<p>Set correct <code>SHA1_CHECK</code> in <code>/usr/local/sbin/build-encryption-key.sh</code> and test if the script gets correctly invoked upon first login as root:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>extbackup$ sudo su -
[WARNING] The LUKS key file (/mnt/ramfs/luks_pw) does not yet exist!
Unlock LUKS encryption key: 
[INFO] The LUKS key file was successfully stored in /mnt/ramfs/luks_pw.
[INFO] SSH private key was successfully decrypted to /mnt/ramfs/id_rsa.decrypted.
</code></pre></div>
<h2 id="luks-encrypt-usb-drive"><a href="#luks-encrypt-usb-drive" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>LUKS encrypt USB drive</h2>
<blockquote>
<p>Repeat the following steps as initial setup for every USB disk you wish to use for your external backups.</p>
</blockquote>
<p>Install <code>cryptsetup</code> if not already installed. Connect the external drive, leave it unmounted, and make note of the device label (e.g. <code>/dev/sdb</code>):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">apt-get</span> <span class="token function">install</span> cryptsetup
$ lsblk
</code></pre></div>
<p>Create a single partition using a favorite partitioning utility (fdisk, gparted,...) that fills the entire drive:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">parted</span> /dev/sdb
<span class="token punctuation">(</span>parted<span class="token punctuation">)</span> mklabel gpt
<span class="token punctuation">(</span>parted<span class="token punctuation">)</span> mkpart primary ext3 <span class="token number">0</span>% <span class="token number">100</span>%
<span class="token punctuation">(</span>parted<span class="token punctuation">)</span> quit
</code></pre></div>
<p>Encrypt the partition (using Cryptsetup encryption option defaults) and use our keyfile instead of interactively providing the password:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ cryptsetup <span class="token parameter variable">--cipher</span> aes-xts-plain64 --key-size <span class="token number">512</span> <span class="token parameter variable">--hash</span> sha256 --iter-time <span class="token number">2000</span> --key-file<span class="token operator">=</span>/mnt/ramfs/luks_pw luksFormat /dev/sdb1
$ cryptsetup luksOpen --key-file<span class="token operator">=</span>/mnt/ramfs/luks_pw /dev/sdb1 sdb1_crypt
</code></pre></div>
<p>Install ext4 filesystem and mount the partition to gain access to the storage:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ mkfs.ext4 <span class="token parameter variable">-E</span> <span class="token assign-left variable">lazy_itable_init</span><span class="token operator">=</span><span class="token number">0</span>,lazy_journal_init<span class="token operator">=</span><span class="token number">0</span> /dev/mapper/sdb1_crypt
$ <span class="token function">mount</span> <span class="token parameter variable">-t</span> ext4 /dev/mapper/sdb1_crypt /mnt/plaintext
$ <span class="token builtin class-name">echo</span> <span class="token parameter variable">-n</span> <span class="token string">"BACKUP_1"</span> <span class="token operator">></span> /mnt/plaintext/LABEL
</code></pre></div>
<p>Before physically disconnecting the drive the partition must be unmounted and the encrypted device must be closed:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">umount</span> /mnt/plaintext
$ cryptsetup luksClose /dev/mapper/sdb1_crypt
</code></pre></div>
<h2 id="luks-mounting"><a href="#luks-mounting" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>LUKS mounting</h2>
<p>Let's create <code>/etc/profile.d/luks-encryption.sh</code> to define the following aliases:</p>
<div class="nuxt-content-highlight"><span class="filename">luks-encryption.sh</span><pre class="line-numbers language-bash"><code><span class="token function-name function">luksMount</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token assign-left variable">DEVNAME</span><span class="token operator">=</span><span class="token variable">${1<span class="token operator">:-</span>sdb}</span>
    <span class="token function">mkdir</span> <span class="token parameter variable">-p</span> /mnt/plaintext
    <span class="token function">parted</span> /dev/<span class="token variable">$DEVNAME</span> print <span class="token operator">></span> /dev/null <span class="token operator">&#x26;&#x26;</span> <span class="token function">sleep</span> <span class="token number">5</span> <span class="token operator">&#x26;&#x26;</span> cryptsetup luksOpen --key-file<span class="token operator">=</span>/mnt/ramfs/luks_pw /dev/<span class="token variable">${DEVNAME}</span><span class="token number">1</span> <span class="token variable">${DEVNAME}</span>1_crypt <span class="token operator">&#x26;&#x26;</span> <span class="token function">mount</span> <span class="token parameter variable">-t</span> ext4 /dev/mapper/<span class="token variable">${DEVNAME}</span>1_crypt /mnt/plaintext
<span class="token punctuation">}</span>
<span class="token builtin class-name">alias</span> mount-extbackup<span class="token operator">=</span>luksMount

<span class="token function-name function">luksUmount</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token assign-left variable">DEVNAME</span><span class="token operator">=</span><span class="token variable">${1<span class="token operator">:-</span>sdb}</span>
    <span class="token function">umount</span> <span class="token parameter variable">-l</span> /mnt/plaintext <span class="token operator">&#x26;&#x26;</span> cryptsetup luksClose /dev/mapper/<span class="token variable">${DEVNAME}</span>1_crypt
<span class="token punctuation">}</span>
<span class="token builtin class-name">alias</span> umount-extbackup<span class="token operator">=</span>luksUmount
</code></pre></div>
<p>The <code>parted</code> command in <code>luksMount()</code> is only used to ensure our USB drive spins up in case it went to sleep.</p>
<p>We can now use those aliases to mount/unmount our external drive to <code>/mnt/plaintext</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ mount-extbackup
$ <span class="token function">cat</span> /mnt/plaintext/LABEL
$ umount-extbackup
</code></pre></div>
<h2 id="secure-rsyncing-over-ssh"><a href="#secure-rsyncing-over-ssh" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Secure rsyncing over SSH</h2>
<h3 id="encrypt-ssh-private-key"><a href="#encrypt-ssh-private-key" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Encrypt SSH private key</h3>
<p>You may have noticed that our <code>build-encryption-key.sh</code> script also tries to decrypt your SSH private key to ramfs upon first login as root. We are going to use our default identity file <code>~/.ssh/id_rsa</code> for rsyncing over ssh. But we don't want to have it stored plaintext anywhere on persistent storage.</p>
<p>If you don't have any SSH public/private keypair yet, create it (<code>ssh-keygen</code> defaults to RSA 2048 bit, but we prefer 4096 bit), with an empty passphrase:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ ssh-keygen <span class="token parameter variable">-b</span> <span class="token number">4096</span> <span class="token parameter variable">-P</span> <span class="token string">''</span>
</code></pre></div>
<p>We are now going to encrypt our private key with OpenSSL, using the same encryption key as we use to encrypt our LUKS devices:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token builtin class-name">cd</span> ~/.ssh/
$ <span class="token function">cat</span> id_rsa <span class="token operator">|</span> openssl enc <span class="token parameter variable">-e</span> -aes-256-cbc <span class="token parameter variable">-a</span> <span class="token parameter variable">-k</span> <span class="token string">"<span class="token variable"><span class="token variable">$(</span><span class="token function">cat</span> /mnt/ramfs/luks_pw<span class="token variable">)</span></span>"</span> <span class="token operator">></span> id_rsa.encrypted
$ <span class="token function">chmod</span> <span class="token number">600</span> id_rsa.encrypted
</code></pre></div>
<p>Let's now secure erase our plaintext private key:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ shred <span class="token parameter variable">-u</span> ~/.ssh/id_rsa
</code></pre></div>
<p>To test decryption, remove LUKS key in ramfs and login again as root (which invokes <code>build-encryption-key.sh</code>):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">rm</span> <span class="token parameter variable">-f</span> /mnt/ramfs/*
$ <span class="token builtin class-name">exit</span>
$ <span class="token function">sudo</span> <span class="token function">su</span> -
<span class="token punctuation">[</span>WARNING<span class="token punctuation">]</span> The LUKS key <span class="token function">file</span> <span class="token punctuation">(</span>/mnt/ramfs/luks_pw<span class="token punctuation">)</span> does not yet exist<span class="token operator">!</span>
Unlock LUKS encryption key: 
<span class="token punctuation">[</span>INFO<span class="token punctuation">]</span> The LUKS key <span class="token function">file</span> was successfully stored <span class="token keyword">in</span> /mnt/ramfs/luks_pw.
<span class="token punctuation">[</span>INFO<span class="token punctuation">]</span> SSH private key was successfully decrypted to /mnt/ramfs/id_rsa.decrypted.
</code></pre></div>
<p>You should then also find <code>/mnt/ramfs/id_rsa.decrypted</code> correctly symlinked to <code>~/.ssh/id_rsa</code>.</p>
<h3 id="limit-rsync-to-a-directory-rrsync"><a href="#limit-rsync-to-a-directory-rrsync" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Limit rsync to a directory (rrsync)</h3>
<p><code>extbackup</code> needs full root permissions to pull data from backup server, but its access should be limited to rsync from a specific directory and not executing any other commands on <code>backup</code>.</p>
<p>There is a simple rsync wrapper for this which comes shipped with Debian's rsync package but first needs to be unpacked/installed:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>backup$ zcat /usr/share/doc/rsync/scripts/rrsync.gz <span class="token operator">></span> /usr/local/bin/rrsync
backup$ <span class="token function">chmod</span> 0755 /usr/local/bin/rrsync
backup$ <span class="token function">ln</span> <span class="token parameter variable">-s</span> /usr/local/bin/rrsync /usr/bin/rrsync
</code></pre></div>
<p>We then add this to <code>/etc/sudoers.d/extbackup</code> on <code>backup</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>%backuppers ALL= NOPASSWD:SETENV: /usr/bin/rrsync
</code></pre></div>
<p>Let's now create a specific user <code>extbackup</code> and add him to group <code>backuppers</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>backup$ adduser extbackup
backup$ adduser extbackup backuppers
</code></pre></div>
<p>Add the public key of <code>extbackup</code> (found in <code>extbackup:~/.ssh/id_rsa.pub</code>) to <code>backup:/home/extbackup/.ssh/authorized_keys</code>, prefixed by the following options:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token assign-left variable">command</span><span class="token operator">=</span><span class="token string">"sudo -E /usr/bin/rrsync -ro /backups/"</span>,no-agent-forwarding,no-port-forwarding,no-pty,no-user-rc,no-X11-forwarding ssh-rsa AAAABxyz<span class="token punctuation">..</span>.
</code></pre></div>
<p>This limits <code>extbackup</code> to only pull data from <code>backup:/backups/</code> directory over rsync. Test-run:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>extbackup$ <span class="token function">rsync</span> <span class="token parameter variable">-aH</span> --numeric-ids <span class="token parameter variable">--delete</span> --delete-before extbackup@backup: /mnt/plaintext/backups
</code></pre></div>
<blockquote>
<p>Explanation: <code>extbackup</code>'s root user rsyncs data from <code>backup:/backups/</code> to <code>/mnt/plaintext</code> (decrypted external USB drive) but only needs to provide the relative path to <code>backup</code> as rsyncing is enforced over rrsync to <code>/backups/</code> directory on the remote side. If we want to pull all data from <code>backup:/backups/</code>, simply provide an empty relative path after the colon.</p>
</blockquote>
<p>Isn't that cool? We can pull data with root rights from our backup server but are limited to one specific directory and are not able to execute any other commands except rsync.</p>
<h2 id="final-backup-script"><a href="#final-backup-script" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Final backup script</h2>
<p>The following script basically decrypts and mounts the external USB device, then rsyncs the whole data from <code>backup:/backups/</code> to it and unmounts / closes the encrypted device right afterwards.</p>
<div class="nuxt-content-highlight"><span class="filename">extbackup.sh</span><pre class="line-numbers language-bash"><code><span class="token shebang important">#!/bin/bash</span>

<span class="token comment">######### CONFIGURATION ############</span>
<span class="token assign-left variable">BKUP_USER</span><span class="token operator">=</span>extbackup
<span class="token assign-left variable">BKUP_SERVER</span><span class="token operator">=</span>backup.example.com
<span class="token assign-left variable">BKUP_SRCDIR</span><span class="token operator">=</span>/backups
<span class="token assign-left variable">TSFORMAT</span><span class="token operator">=</span><span class="token string">"%Y-%m-%d %H:%M:%S"</span>
<span class="token assign-left variable">RSYNC_CMD</span><span class="token operator">=</span><span class="token string">"/usr/bin/nice -n19 /usr/bin/ionice -c3 /usr/bin/rsync"</span>
<span class="token assign-left variable">KEYFILE</span><span class="token operator">=</span>/mnt/ramfs/luks_pw
<span class="token assign-left variable">USB_DEVICE</span><span class="token operator">=</span>/dev/sdb
<span class="token assign-left variable">USB_PARTITION</span><span class="token operator">=</span>/dev/sdb1
<span class="token assign-left variable">MAPPER</span><span class="token operator">=</span>sdb1_crypt
<span class="token assign-left variable">MNT_PLAINTEXT</span><span class="token operator">=</span>/mnt/plaintext
<span class="token comment">####################################</span>

<span class="token function-name function">printinfo</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"[<span class="token variable"><span class="token variable">`</span><span class="token function">date</span> +<span class="token string">"<span class="token variable">$TSFORMAT</span>"</span><span class="token variable">`</span></span>] <span class="token variable">$1</span>"</span>
<span class="token punctuation">}</span>

<span class="token function-name function">printwarn</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token builtin class-name">echo</span> <span class="token string">"[<span class="token variable"><span class="token variable">`</span><span class="token function">date</span> +<span class="token string">"<span class="token variable">$TSFORMAT</span>"</span><span class="token variable">`</span></span>] WARNING: <span class="token variable">$1</span>"</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token parameter variable">--color</span> <span class="token string">"WARNING"</span>
<span class="token punctuation">}</span>

<span class="token function-name function">printerr</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token operator">></span><span class="token file-descriptor important">&#x26;2</span> <span class="token builtin class-name">echo</span> <span class="token string">"[<span class="token variable"><span class="token variable">`</span><span class="token function">date</span> +<span class="token string">"<span class="token variable">$TSFORMAT</span>"</span><span class="token variable">`</span></span>] ERROR: <span class="token variable">$1</span>"</span>
<span class="token punctuation">}</span>

<span class="token function-name function">errquit</span><span class="token punctuation">(</span><span class="token punctuation">)</span> <span class="token punctuation">{</span>
    <span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token operator">!</span> <span class="token parameter variable">-z</span> <span class="token string">"<span class="token variable">$1</span>"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
        <span class="token operator">></span><span class="token file-descriptor important">&#x26;2</span> <span class="token builtin class-name">echo</span> <span class="token string">"[<span class="token variable"><span class="token variable">`</span><span class="token function">date</span> +<span class="token string">"<span class="token variable">$TSFORMAT</span>"</span><span class="token variable">`</span></span>] ERROR: <span class="token variable">$1</span>"</span>
    <span class="token keyword">fi</span>
    <span class="token builtin class-name">exit</span> <span class="token number">1</span>
<span class="token punctuation">}</span>

<span class="token comment"># check if key file exists</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token operator">!</span> <span class="token parameter variable">-f</span> <span class="token variable">$KEYFILE</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    errquit <span class="token string">"The LUKS key file <span class="token variable">$KEYFILE</span> does not exist yet. Please run build-encryption-key.sh first!"</span>
<span class="token keyword">fi</span>

<span class="token comment"># create mountpoints if they don't exist yet</span>
<span class="token function">mkdir</span> <span class="token parameter variable">-p</span> <span class="token variable">$MNT_PLAINTEXT</span>

<span class="token comment"># First, make sure $USB_DEVICE will wake up prior to mounting $USB_PARTITION</span>
<span class="token comment"># (using sfdisk as a simple workaround)</span>
<span class="token comment">#sfdisk -d $USB_DEVICE > /dev/null</span>
<span class="token function">parted</span> <span class="token variable">$USB_DEVICE</span> print <span class="token operator">></span> /dev/null
<span class="token function">sleep</span> <span class="token number">5</span>

<span class="token comment"># open luks device</span>
cryptsetup luksOpen --key-file<span class="token operator">=</span><span class="token variable">$KEYFILE</span> <span class="token variable">$USB_PARTITION</span> <span class="token variable">$MAPPER</span>

<span class="token comment"># abort on errors</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token string">"<span class="token variable">$?</span>"</span> <span class="token parameter variable">-ne</span> <span class="token string">"0"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
  errquit <span class="token string">"Could not open encrypted device <span class="token variable">$USB_PARTITION</span>. Backup script aborted!!!"</span>
<span class="token keyword">fi</span>

<span class="token comment"># mount mapped device</span>
<span class="token function">mount</span> <span class="token parameter variable">-t</span> ext4 /dev/mapper/<span class="token variable">$MAPPER</span> <span class="token variable">$MNT_PLAINTEXT</span>

<span class="token comment"># abort, if $MNT_PLAINTEXT is already mounted</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token string">"<span class="token variable">$?</span>"</span> <span class="token parameter variable">-ne</span> <span class="token string">"0"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
  errquit <span class="token string">"Could not mount /dev/mapper/<span class="token variable">$MAPPER</span> to <span class="token variable">$MNT_PLAINTEXT</span>. Backup script aborted!!!"</span>
<span class="token keyword">fi</span>

<span class="token comment"># abort on missing label</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token operator">!</span> <span class="token parameter variable">-f</span> <span class="token variable">$MNT_PLAINTEXT</span>/LABEL <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
  errquit <span class="token string">"Disk label (<span class="token variable">$MNT_PLAINTEXT</span>/LABEL) does not exist. Backup script aborted!!!"</span>
<span class="token keyword">fi</span>

<span class="token comment"># TEMP</span>
<span class="token builtin class-name">cd</span> <span class="token variable">$MNT_PLAINTEXT</span>/
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token parameter variable">-d</span> backup <span class="token punctuation">]</span> <span class="token operator">&#x26;&#x26;</span> <span class="token punctuation">[</span> <span class="token operator">!</span> <span class="token parameter variable">-h</span> backup <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    printwarn <span class="token string">"removing legacy <span class="token variable">$MNT_PLAINTEXT</span>/backup first ..."</span>
    <span class="token function">rm</span> <span class="token parameter variable">-rf</span> backup
<span class="token keyword">fi</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token parameter variable">-d</span> remotebackup <span class="token punctuation">]</span> <span class="token operator">&#x26;&#x26;</span> <span class="token punctuation">[</span> <span class="token operator">!</span> <span class="token parameter variable">-e</span> backup <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
    printinfo <span class="token string">"Creating symlink <span class="token variable">$MNT_PLAINTEXT</span>/backup -> <span class="token variable">$MNT_PLAINTEXT</span>/remotebackup ..."</span>
    <span class="token function">ln</span> <span class="token parameter variable">-sf</span> remotebackup backup
<span class="token keyword">fi</span>

printinfo <span class="token string">"start syncing backups from <span class="token variable">$BKUP_SERVER</span>:<span class="token variable">$BKUP_SRCDIR</span> ..."</span>
<span class="token assign-left variable">RSYNC_PARAMS</span><span class="token operator">=</span><span class="token string">""</span>
<span class="token variable">$RSYNC_CMD</span> <span class="token parameter variable">-aH</span> --numeric-ids <span class="token parameter variable">-e</span> <span class="token function">ssh</span> <span class="token parameter variable">--delete</span> --delete-before <span class="token variable">$BKUP_USER</span>@<span class="token variable">$BKUP_SERVER</span><span class="token builtin class-name">:</span> <span class="token variable">$MNT_PLAINTEXT</span>/backups

printinfo <span class="token string">"DONE."</span>
<span class="token builtin class-name">echo</span>
<span class="token builtin class-name">echo</span>

<span class="token comment"># print disk usage</span>
<span class="token assign-left variable">DISK_LABEL</span><span class="token operator">=</span><span class="token variable"><span class="token variable">`</span><span class="token function">cat</span> $MNT_PLAINTEXT/LABEL<span class="token variable">`</span></span>
<span class="token builtin class-name">echo</span> <span class="token string">"------------------------------"</span>
<span class="token builtin class-name">echo</span> <span class="token string">"DISK LABEL: <span class="token variable">$DISK_LABEL</span>"</span>
<span class="token builtin class-name">echo</span> <span class="token string">"------------------------------"</span>
<span class="token function">df</span> <span class="token parameter variable">-h</span> <span class="token operator">|</span> <span class="token function">grep</span> /mnt/

<span class="token comment"># set modification timestamp for LABEL (as reference for last successful backup run)</span>
<span class="token function">touch</span> <span class="token variable">$MNT_PLAINTEXT</span>/LABEL

<span class="token comment"># make sure file system is idle</span>
<span class="token function">sync</span>
<span class="token function">sleep</span> <span class="token number">5</span>

<span class="token comment"># unmount mapped device</span>
<span class="token comment"># Lazy  unmount. Detach the filesystem from the filesystem hierarchy now,</span>
<span class="token comment"># and cleanup all references to the filesystem as soon as it is</span>
<span class="token comment"># not busy anymore</span>
<span class="token function">umount</span> <span class="token parameter variable">-l</span> <span class="token variable">$MNT_PLAINTEXT</span>

<span class="token comment"># abort on errors</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token string">"<span class="token variable">$?</span>"</span> <span class="token parameter variable">-ne</span> <span class="token string">"0"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
  <span class="token builtin class-name">echo</span> <span class="token string">"Could not unmount <span class="token variable">$MNT_PLAINTEXT</span>. Backup script aborted!!!"</span>
  <span class="token builtin class-name">echo</span> <span class="token string">"lsof output:"</span>
  <span class="token builtin class-name">echo</span> <span class="token string">""</span>
  <span class="token function">lsof</span>
  <span class="token builtin class-name">exit</span> <span class="token number">1</span>
<span class="token keyword">fi</span>

<span class="token comment"># make sure file system is idle</span>
<span class="token function">sync</span>
<span class="token function">sleep</span> <span class="token number">60</span>

<span class="token comment"># close encrypted device</span>
cryptsetup luksClose /dev/mapper/<span class="token variable">$MAPPER</span>

<span class="token comment"># abort on errors</span>
<span class="token keyword">if</span> <span class="token punctuation">[</span> <span class="token string">"<span class="token variable">$?</span>"</span> <span class="token parameter variable">-ne</span> <span class="token string">"0"</span> <span class="token punctuation">]</span><span class="token punctuation">;</span> <span class="token keyword">then</span>
  <span class="token builtin class-name">echo</span> <span class="token string">"Could not close encrypted LUKS device /dev/mapper/<span class="token variable">$MAPPER</span>!!!"</span>
  <span class="token builtin class-name">echo</span> <span class="token string">"lsof output:"</span>
  <span class="token builtin class-name">echo</span> <span class="token string">""</span>
  <span class="token comment"># http://askubuntu.com/questions/429612/device-mapper-remove-ioctl-on-luks-xxxx-failed-device-or-resource-busy</span>
  <span class="token comment"># the following sed replacement won't work...</span>
  <span class="token comment"># dmsetup ls | grep sdb1_crypt | sed 's/.*\((\d+):(\d+)\)/\1,\2/'</span>
  <span class="token function">lsof</span> <span class="token operator">|</span> <span class="token function">grep</span> <span class="token number">254,2</span>
  <span class="token builtin class-name">exit</span> <span class="token number">1</span>
<span class="token keyword">fi</span>
</code></pre></div>
<p>Run this as a weekly cronjob, e.g. via <code>/etc/crontab</code>, starting Sat morning:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>00 07   * * 6   root    extbackup.sh
</code></pre></div>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="backup" term="backup"/>
        <category label="security" term="security"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Proxmox VE 4.x OpenVZ to LXC Migration]]></title>
        <id>https://pipo.blog/articles/20160904-proxmoxve4-openvz-to-lxc</id>
        <link href="https://pipo.blog/articles/20160904-proxmoxve4-openvz-to-lxc"/>
        <updated>2016-09-04T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Migrating all OpenVZ containers to LXC on Proxmox VE 4.x.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/containers.jpg"></p>
                      <p>At <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime Webhosting</a> we chose <a href="https://proxmox.com/en/proxmox-ve" rel="nofollow noopener noreferrer" target="_blank">ProxmoxVE</a> as our favorite virtualization platform and are running a bunch of  OpenVZ containers for many years now, with almost zero issues. We very  much welcome the small overhead and simplicity of container based  virtualization and wouldn’t want to move to anything else. ProxmoxVE  added <strong>ZFS support</strong> by integrating <a href="https://zfsonlinux.org/" rel="nofollow noopener noreferrer" target="_blank">ZFSonLinux</a> back in Feb 2015 with the great ProxmoxVE 3.4 release – which actually  would have deserved to bump its major version because of this killer  feature.</p>
<!--more-->
<h2 id="openvz-on-pve"><a href="#openvz-on-pve" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>OpenVZ on PVE</h2>
<p>Previously we have been running our OpenVZ containers on plain <a href="https://www.debian.org/" rel="nofollow noopener noreferrer" target="_blank">Debian Linux</a> boxes. But with Debian Wheezy (7.0) the Debian community decided to no  longer include a kernel which has been patched with the OpenVZ  extensions. Switching to ProxmoxVE at that time was a piece of cake as  ProxmoxVE was just a plain Debian Linux with a RHEL kernel, OpenVZ and  KVM support, and some nice web interface to manage the containers and  VMs. Actually, we never really needed the ProxmoxVE web interface (GUIs  always suck!), but that one is quite lightweight and very well  integrated. If you don’t like it, use the provided CLI tools – ProxmoxVE does not force you to use the web interface at all.</p>
<p>In 2014 I did a review for <a href="https://www.packtpub.com/product/proxmox-high-availability/9781783980888" rel="nofollow noopener noreferrer" target="_blank">Proxmox High Availability (by PACKT publishing)</a>. But at Onlime, we never employed any cluster technologies to date. I  was always looking for simple solutions without unnecessary complexity.  ProxmoxVE with OpenVZ on ZFS pools was the perfect match. We are  replicating our containers from one host node to another every 10mins  with simple and super fast <code>zfs send|receive</code> via a modified version of <a href="http://www.bolthole.com/solaris/zrep/" rel="nofollow noopener noreferrer" target="_blank">ZREP</a>.</p>
<p>In ProxmoxVE 4, OpenVZ support was removed in favor of LXC. Both, KVM and LXC are built into any newer Linux kernel, so in the long term (and due to the nature of OpenVZ as a huge kernel patch which is hard to  maintain) it was clear we had to give up OpenVZ. Even though the OpenVZ  developers are currently focusing on merging the OpenVZ and Virtuozzo  source codebase and <a href="https://docs.openvz.org/virtuozzo_7_readme.webhelp/_what_8217_s_new.html" rel="nofollow noopener noreferrer" target="_blank">have just released OpenVZ 7.0</a> I don’t give OpenVZ a long future any more. In comparison to LXC, the  tools for OpenVZ are still much more mature and powerful, but lately LXC made a lot of progress and having it perfectly integrated into  ProxmoxVE is a great step forward. So, we have decided to migrate to  LXC.</p>
<p>(Sorry about the long preface. You probably don’t care about history reading this blog post. I am getting to the point now…)</p>
<h2 id="the-clumsy-official-migration-path"><a href="#the-clumsy-official-migration-path" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>The clumsy (official) migration path</h2>
<p>What I cannot believe and what prevented me to look into LXC on ProxmoxVE up to now is the <strong>clumsy migration path that ProxmoxVE suggests on <a href="https://pve.proxmox.com/wiki/Convert_OpenVZ_to_LXC" rel="nofollow noopener noreferrer" target="_blank">ProxmoxVE Wiki: Convert OpenVZ to LXC</a></strong>. Basically they suggest to dump and restore a full container as follows:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>pve1$ vzctl stop <span class="token number">100</span> <span class="token operator">&#x26;&#x26;</span> vzdump <span class="token number">100</span> <span class="token parameter variable">-storage</span> <span class="token builtin class-name">local</span>
pve1$ <span class="token function">scp</span> /var/lib/vz/dump/vzdump-openvz-100.tar pve2:/var/lib/vz/dump/
pve2$ pct restore <span class="token number">100</span> /var/lib/vz/dump/vzdump-openvz-100.tar
</code></pre></div>
<p>Srsly? Do you guys only have containers with sizes below 1GB and  don’t care about downtime at all? Real world looks different and I would not even go this migration path with a 5GB container. I don’t even want to think about those containers with ~ 1TB of data.</p>
<h2 id="recommended-migration-to-lxc"><a href="#recommended-migration-to-lxc" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Recommended migration to LXC</h2>
<p>If you are running OpenVZ containers on ProxmoxVE, sure you have some easy solution to migrate your containers from one host node to another. As I said, we are using <a href="http://www.bolthole.com/solaris/zrep/" rel="nofollow noopener noreferrer" target="_blank">ZREP</a> for container replication and have written a wrapper script that  migrates a container to its failover host node. That’s what I suggest  and please forget about live migration – it never really was working in  OpenVZ (well, it was, but I am talking about a really stable solution  that never fails) and LXC lacks any kind of hot/live migration. Doing a  ZREP presync, stopping the container on the source host node, doing the  main ZREP sync, finally starting the container on the destination host  node is just a matter of seconds, usually causes a downtime of 10s up to 30s max (on a larger container).</p>
<p>Here’s the migration path I suggest for any ProxmoxVE setup with containers running on ZFS pools:</p>
<ol>
<li><strong>move all OpenVZ containers</strong> to the primary host node</li>
<li>upgrade the <strong>secondary host node to ProxmoxVE 4.x</strong></li>
<li>prepare the <strong>new LXC container</strong> on the secondary host node, don’t start it yet</li>
<li><strong>replace the newly created ZFS pool</strong> with the existing (which is still in sync with ZREP replication)</li>
<li>do a <code>vzctl stop $VEID &#x26;&#x26; zrep failover rpool/ROOT/pve-$VEID</code> to <strong>activate (rw) the ZFS pool on the secondary host node</strong></li>
<li><strong>start the LXC container</strong> on the secondary host node</li>
<li>once you have migrated all containers to LXC on the secondary host node, upgrade the primary host node to ProxmoxVE 4.x</li>
</ol>
<p>On a new or upgraded ProxmoxVE host node, first configure ZFS storage:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>pve2$ zfs create rpool/zfsdisks

<span class="token comment"># turn on posixacl by default for all containers (inherited)</span>
pve2$ zfs <span class="token builtin class-name">set</span> <span class="token assign-left variable">acltype</span><span class="token operator">=</span>posixacl rpool/zfsdisks

pve2$ pvesm <span class="token function">add</span> zfspool zfsvols <span class="token parameter variable">-pool</span> rpool/zfsdisks <span class="token parameter variable">-content</span> images,rootdir <span class="token parameter variable">-sparse</span>
</code></pre></div>
<p>Check the storage configuration, <code>/etc/pve/storage.cfg</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">/etc/pve/storage.cfg</span><pre class="line-numbers language-text"><code>dir: local
    path /var/lib/vz
    maxfiles 0
    content rootdir,iso,images,vztmpl

zfspool: zfsvols
    pool rpool/zfsdisks
    content images,rootdir
    sparse
</code></pre></div>
<p>Creating a new LXC container can be done via ProxmoxVE web interface or (my preference) via CLI. We are using the new <strong>zfsvols</strong> storage – <code>pct</code> takes care of creating the ZFS pool for you:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>pve2$ pct create <span class="token number">100</span> local:vztmpl/debian-8.0-standard_8.4-1_amd64.tar.gz <span class="token punctuation">\</span>
 <span class="token parameter variable">-hostname</span> demo.onlime.ch <span class="token punctuation">\</span>
 <span class="token parameter variable">-cpulimit</span> <span class="token number">2</span> <span class="token punctuation">\</span>
 <span class="token parameter variable">-cpuunits</span> <span class="token number">1024</span> <span class="token punctuation">\</span>
 <span class="token parameter variable">-memory</span> <span class="token number">4096</span> <span class="token punctuation">\</span>
 <span class="token parameter variable">-swap</span> <span class="token number">1024</span> <span class="token punctuation">\</span>
 <span class="token parameter variable">-ostype</span> debian <span class="token punctuation">\</span>
 <span class="token parameter variable">-rootfs</span> zfsvols:subvol-100-disk-1,acl<span class="token operator">=</span><span class="token number">1</span> <span class="token punctuation">\</span>
 <span class="token parameter variable">-net0</span> <span class="token assign-left variable">name</span><span class="token operator">=</span>eth0,bridge<span class="token operator">=</span>vmbr0,gw<span class="token operator">=</span>X.X.X.X,ip<span class="token operator">=</span>X.X.X.100/25 <span class="token punctuation">\</span>
 <span class="token parameter variable">-storage</span> zfsvols
</code></pre></div>
<p>You probably want to write a short helper script to simplify this step as most parameters will stay the same for all containers.</p>
<p>Do not try to change the <strong>subvol-VMID-disk-1</strong> naming scheme of the LXC container, see <a href="https://forum.proxmox.com/threads/change-default-mountpoint-volume-name-for-lxc-container-on-zfs-storage.28915/#post-145295" rel="nofollow noopener noreferrer" target="_blank">Dietmar Maurer’s comment</a>:</p>
<blockquote>
<p>I guess we hardcoded that naming scheme a several places, so it would be hard to change that. We also use disk names to encode  some informations, so we cannot allow arbitrary name changes.</p>
</blockquote>
<p>We now have both ZFS pools on the secondary host node:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>pve2$ zfs list
NAME  USED  AVAIL  REFER  MOUNTPOINT
rpool/ROOT/pve-100  560M  436G  529M  /var/lib/vz/private/184
rpool/zfsdisks/subvol-100-disk-1  560M  436G  529M  /rpool/zfsdisks/subvol-100-disk-1
</code></pre></div>
<p>Migration is now that simple, just replace the ZFS pool with your  existing data (without dumping/backupping/restoring anything!):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>pve2$ <span class="token assign-left variable">VMID</span><span class="token operator">=</span><span class="token number">100</span>
pve2$ zfs destroy <span class="token parameter variable">-r</span> rpool/zfsdisks/subvol-<span class="token variable">$VMID</span>-disk-1
pve2$ zfs <span class="token function">rename</span> rpool/ROOT/pve-<span class="token variable">$VEID</span> rpool/zfsdisks/subvol-<span class="token variable">$VMID</span>-disk-1
pve2$ zfs <span class="token builtin class-name">set</span> <span class="token assign-left variable">mountpoint</span><span class="token operator">=</span>/rpool/zfsdisks/subvol-<span class="token variable">$VMID</span>-disk-1 rpool/zfsdisks/subvol-<span class="token variable">$VMID</span>-disk-1
</code></pre></div>
<p>After adjusting the <code>zrep:dest-fs</code> ZFS property on both  source and destination host, you can stop the OpenVZ container,  replicate it over, and start it as LXC container on the destination  side:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>pve1$ zfs <span class="token builtin class-name">set</span> zrep:dest-fs<span class="token operator">=</span>rpool/zfsdisks/subvol-100-disk-1 rpool/ROOT/pve-100
pve2$ zfs <span class="token builtin class-name">set</span> zrep:dest-fs<span class="token operator">=</span>rpool/zfsdisks/subvol-100-disk-1 rpool/zfsdisks/subvol-100-disk-1

pve1$ vzctl stop <span class="token number">100</span> <span class="token operator">&#x26;&#x26;</span> zrep failover rpool/ROOT/pve-100 <span class="token operator">&#x26;&#x26;</span> <span class="token function">ssh</span> pve2 pct start <span class="token number">100</span>
</code></pre></div>
<p>That’s it. We didn’t have to copy a single bit of data and we just  had the regular (very minimal) downtime of a container failover like we  did it before in our OpenVZ-to-OpenVZ environment.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="openvz" term="openvz"/>
        <category label="lxc" term="lxc"/>
        <category label="proxmoxve" term="proxmoxve"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Install Composer with Ansible, the lean way]]></title>
        <id>https://pipo.blog/articles/20160607-ansible-composer-install</id>
        <link href="https://pipo.blog/articles/20160607-ansible-composer-install"/>
        <updated>2016-06-07T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Install Composer without running the installer, using an Ansible playbook.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/composer.jpg"></p>
                      <p>Every PHP developer needs <a href="https://getcomposer.org/" rel="nofollow noopener noreferrer" target="_blank">Composer</a> and as a webhosting company at <a href="https://www.onlime.ch/" rel="nofollow noopener noreferrer" target="_blank">Onlime GmbH</a>, sure we had to provide Composer binary to every customer, deploying it to every webserver. But how come the recommended Composer installation for <a href="https://getcomposer.org/doc/00-intro.md#installation-linux-unix-macos" rel="nofollow noopener noreferrer" target="_blank">Linux/Unix/macOS</a> is so clunky, only providing the latest <code>composer.phar</code> through an installer?</p>
<p>Sure, installers are fine, but not for a sysadmin who likes to keep things simple and fully manage his infrastructure with <a href="https://www.ansible.com/" rel="nofollow noopener noreferrer" target="_blank">Ansible</a>. Installing Composer should be nothing more than deploying the latest <code>composer.phar</code>, period. But the author of Composer somehow forgot to provide us a download URL for the latest stable version. (Sorry <a href="https://seld.be/" rel="nofollow noopener noreferrer" target="_blank">Jordi Boggiano</a>, don't want to blame you - maybe I just overlooked it and should have asked you via DM. But writing that small Ansible playbook was still faster than looking any further.)</p>
<!--more-->
<p>So, I would like to present you a super simple way of grabbing the latest Composer version, download and deploy <code>composer.phar</code>.</p>
<p>Luckily, the author of Composer provides us with a nice JSON that contains all version information (ok, that's nice!):</p>
<ul>
<li><a href="https://getcomposer.org/versions" rel="nofollow noopener noreferrer" target="_blank">getcomposer.org/versions</a></li>
</ul>
<p>We're only interested in <code>stable</code> version:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-json"><code><span class="token punctuation">{</span>
  <span class="token property">"stable"</span><span class="token operator">:</span> <span class="token punctuation">[</span>
    <span class="token punctuation">{</span>
      <span class="token property">"path"</span><span class="token operator">:</span> <span class="token string">"/download/1.1.2/composer.phar"</span><span class="token punctuation">,</span>
      <span class="token property">"version"</span><span class="token operator">:</span> <span class="token string">"1.1.2"</span><span class="token punctuation">,</span>
      <span class="token property">"min-php"</span><span class="token operator">:</span> <span class="token number">50300</span>
    <span class="token punctuation">}</span>
  <span class="token punctuation">]</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>So here is my Ansible playbook, that grabs the stable version from <a href="https://getcomposer.org/versions" rel="nofollow noopener noreferrer" target="_blank">getcomposer.org/versions</a> and resolves the download URL from there, then deploys <code>composer.phar</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">install-composer.yml</span><pre class="line-numbers language-yaml"><code><span class="token punctuation">---</span>
<span class="token punctuation">-</span> <span class="token key atrule">hosts</span><span class="token punctuation">:</span> webservers
  
  <span class="token key atrule">tasks</span><span class="token punctuation">:</span>
    <span class="token comment"># Install Composer the lightweight way (without using installer)</span>
    <span class="token comment"># https://getcomposer.org/download</span>
    <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Composer <span class="token punctuation">|</span> Grab release information
      <span class="token key atrule">uri</span><span class="token punctuation">:</span>
        <span class="token key atrule">url</span><span class="token punctuation">:</span> https<span class="token punctuation">:</span>//getcomposer.org/versions
        <span class="token key atrule">body_format</span><span class="token punctuation">:</span> json
      <span class="token key atrule">register</span><span class="token punctuation">:</span> composer_releases

    <span class="token punctuation">-</span> <span class="token key atrule">set_fact</span><span class="token punctuation">:</span>
        <span class="token key atrule">composer_version</span><span class="token punctuation">:</span> <span class="token string">'{{ composer_releases.json.stable.0.version }}'</span>
        <span class="token key atrule">composer_download_url</span><span class="token punctuation">:</span> <span class="token string">'https://getcomposer.org{{ composer_releases.json.stable.0.path }}'</span>
      <span class="token key atrule">when</span><span class="token punctuation">:</span> not ansible_check_mode <span class="token comment"># composer_releases is not registered</span>

    <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Composer <span class="token punctuation">|</span> Install Composer <span class="token punctuation">{</span><span class="token punctuation">{</span> composer_version <span class="token punctuation">}</span><span class="token punctuation">}</span>
      <span class="token key atrule">get_url</span><span class="token punctuation">:</span>
        <span class="token key atrule">url</span><span class="token punctuation">:</span> <span class="token string">'{{ composer_download_url }}'</span>
        <span class="token key atrule">dest</span><span class="token punctuation">:</span> /usr/local/bin/composer.phar
        <span class="token key atrule">mode</span><span class="token punctuation">:</span> <span class="token number">0755</span>
      <span class="token key atrule">when</span><span class="token punctuation">:</span> not ansible_check_mode <span class="token comment"># composer_releases is not registered</span>

    <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Composer <span class="token punctuation">|</span> Symlink composer.phar
      <span class="token key atrule">file</span><span class="token punctuation">:</span>
        <span class="token key atrule">src</span><span class="token punctuation">:</span> composer.phar
        <span class="token key atrule">dest</span><span class="token punctuation">:</span> /usr/local/bin/composer
        <span class="token key atrule">state</span><span class="token punctuation">:</span> link
      <span class="token key atrule">when</span><span class="token punctuation">:</span> not ansible_check_mode <span class="token comment"># composer_releases is not registered</span>
</code></pre></div>
<p>You could then run this Ansible playbook as follows, limiting it to a single server:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ ansible-playbook install-composer.yml <span class="token parameter variable">-l</span> myhost <span class="token parameter variable">-D</span>
</code></pre></div>
<p>done. That was easy.</p>
<blockquote>
<p><strong>UPDATE 2021-08-22:</strong></p>
<p>I never looked into this again, as above solution was working perfectly fine for the last years. Also, I am upgrading Composer with a frequent cronjob using <code>composer -q self-update</code>, which also works flawlessly. But in the meantime, the authors of Composer published this FAQ:</p>
<p><a href="https://getcomposer.org/doc/faqs/how-to-install-composer-programmatically.md" rel="nofollow noopener noreferrer" target="_blank">How do I install Composer programmatically?</a></p>
<p>That solution still looks to clunky in my eyes. But on the <a href="https://getcomposer.org/download/" rel="nofollow noopener noreferrer" target="_blank">Composer download page</a>, you'll now finally find a direct link to the latest stable version, including the sha256:</p>
<ul>
<li><a href="https://getcomposer.org/download/latest-stable/composer.phar" rel="nofollow noopener noreferrer" target="_blank">Latest Stable</a> (<a href="https://getcomposer.org/download/latest-stable/composer.phar.sha256" rel="nofollow noopener noreferrer" target="_blank">sha256</a> / <a href="https://getcomposer.org/download/latest-stable/composer.phar.sha256sum" rel="nofollow noopener noreferrer" target="_blank">sha256sum</a>)</li>
</ul>
<p>So, forget about this whole article (hey, it dates back to 2016!) and replace above Ansible playbook by this simple one:</p>
<div class="nuxt-content-highlight"><span class="filename">install-composer.yml</span><pre class="line-numbers language-yaml"><code><span class="token punctuation">-</span> <span class="token key atrule">hosts</span><span class="token punctuation">:</span> webservers
  
  <span class="token key atrule">vars</span><span class="token punctuation">:</span>
    <span class="token key atrule">composer_url</span><span class="token punctuation">:</span> https<span class="token punctuation">:</span>//getcomposer.org/download/latest<span class="token punctuation">-</span>stable/composer.phar

  <span class="token key atrule">tasks</span><span class="token punctuation">:</span>
    <span class="token comment"># Install Composer the lightweight way (without using installer)</span>
    <span class="token comment"># https://getcomposer.org/download</span>
    <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Composer <span class="token punctuation">|</span> Install Composer latest<span class="token punctuation">-</span>stable
      <span class="token key atrule">get_url</span><span class="token punctuation">:</span>
        <span class="token key atrule">url</span><span class="token punctuation">:</span> <span class="token string">'{{ composer_url }}'</span>
        <span class="token key atrule">checksum</span><span class="token punctuation">:</span> <span class="token string">'sha256:{{ composer_url }}.sha256sum'</span>
        <span class="token key atrule">dest</span><span class="token punctuation">:</span> /usr/local/bin/composer.phar
        <span class="token key atrule">mode</span><span class="token punctuation">:</span> <span class="token number">0755</span>

    <span class="token punctuation">-</span> <span class="token key atrule">name</span><span class="token punctuation">:</span> Composer <span class="token punctuation">|</span> Symlink composer.phar
      <span class="token key atrule">file</span><span class="token punctuation">:</span>
        <span class="token key atrule">src</span><span class="token punctuation">:</span> composer.phar
        <span class="token key atrule">dest</span><span class="token punctuation">:</span> /usr/local/bin/composer
        <span class="token key atrule">state</span><span class="token punctuation">:</span> link
</code></pre></div>
<p>I still keep this article online, as it shows how easy it is to parse a JSON with Ansible, even from a remote location.</p>
</blockquote>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="php" term="php"/>
        <category label="ansible" term="ansible"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Setting up PHP & MySQL on OS X Yosemite]]></title>
        <id>https://pipo.blog/articles/20141020-osx-yosemite-php-mysql</id>
        <link href="https://pipo.blog/articles/20141020-osx-yosemite-php-mysql"/>
        <updated>2014-10-20T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Set up a dev environment on OS X Yosemite with PHP, MySQL, Apache.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/osx-yosemite-php.png"></p>
                      <p>How to set up a dev environment on OS X Yosemite (10.10) with <a href="https://brew.sh/" rel="nofollow noopener noreferrer" target="_blank">Homebrew</a>, PHP, MySQL, and Apache.</p>
<!--more-->
<h2 id="homebrew"><a href="#homebrew" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Homebrew</h2>
<p><a href="https://brew.sh/" rel="nofollow noopener noreferrer" target="_blank">Homebrew</a> is a package manager for OS X. Install it, as we'll need it later:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ /bin/bash <span class="token parameter variable">-c</span> <span class="token string">"<span class="token variable"><span class="token variable">$(</span><span class="token function">curl</span> <span class="token parameter variable">-fsSL</span> https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh<span class="token variable">)</span></span>"</span>
</code></pre></div>
<p>You'll also need the <strong>Xcode command line tools</strong> – at least version 6.1. Xcode is available on the <a href="https://developer.apple.com/xcode/downloads/" rel="nofollow noopener noreferrer" target="_blank">Mac App Store</a> as a free download. Don't forget to start up Xcode at least once after initial installation.</p>
<p>Initially and after every major OS X upgrade, you might need to reinstall the Xcode command line tools and <strong>accept the license aggreement</strong>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ xcode-select <span class="token parameter variable">--install</span>
$ <span class="token function">sudo</span> xcodebuild <span class="token parameter variable">-license</span>
</code></pre></div>
<p>Also, don't forget to create this missing symlink after upgrading to a new major OSX version:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">sudo</span> <span class="token function">ln</span> <span class="token parameter variable">-s</span> /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain /Applications/Xcode.app/Contents/Developer/Toolchains/OSX10.10.xctoolchain
</code></pre></div>
<p>Once Homebrew is set up and you want to upgrade outdated packages, run:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ brew update
$ brew upgrade
</code></pre></div>
<p>Remember to also run <code>brew update &#x26;&#x26; brew upgrade</code> after upgrading to a new major OS X version.</p>
<p>List all installed packages:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ brew list <span class="token parameter variable">--versions</span>
</code></pre></div>
<p>You should never be required to run <code>brew</code> as root using <code>sudo</code>!</p>
<h2 id="php"><a href="#php" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>PHP</h2>
<p>Apple ships PHP 5.5.14 with OS X Yosemite but we're not going to use it and install the <strong>latest PHP 5.6</strong> instead via Homebrew.</p>
<p>Tap <code>homebrew/homebrew-php</code> and all its dependencies (<code>homebrew/homebrew-binary</code> actually is no dependency but we might need it for binary PHP extensions):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ brew tap homebrew/dupes
$ brew tap homebrew/versions
$ brew tap homebrew/homebrew-php
$ brew tap homebrew/homebrew-binary
</code></pre></div>
<p>Install PHP 5.6:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ brew <span class="token function">install</span> php56 --without-snmp --with-cgi --with-debug --with-homebrew-openssl --with-imap --with-intl --with-mysql
</code></pre></div>
<p>Install some PHP extensions:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ brew <span class="token function">install</span> php56-mcrypt
$ brew <span class="token function">install</span> php56-xdebug
$ brew <span class="token function">install</span> php56-imagick
</code></pre></div>
<p>Edit <code>/usr/local/etc/php/5.6/php.ini</code> and change settings appropriately. At a minimum, you should change:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-ini"><code><span class="token key attr-name">date.timezone</span> <span class="token punctuation">=</span> <span class="token value attr-value">"<span class="token inner-value">Europe/Zurich</span>"</span>
<span class="token key attr-name">error_reporting</span>  <span class="token punctuation">=</span>  <span class="token value attr-value">E_ALL</span>
<span class="token key attr-name">display_errors</span> <span class="token punctuation">=</span> <span class="token value attr-value">On</span>
<span class="token key attr-name">pdo_mysql.default_socket</span><span class="token punctuation">=</span>
<span class="token key attr-name">mysql.default_socket</span><span class="token punctuation">=</span>
</code></pre></div>
<p>We don't need to restart Apache after any modifications in <code>php.ini</code> as we are going to run PHP as CGI.</p>
<h2 id="composer"><a href="#composer" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Composer</h2>
<ul>
<li><a href="http://getcomposer.org/" rel="nofollow noopener noreferrer" target="_blank">Composer</a></li>
<li><a href="http://packagist.org/" rel="nofollow noopener noreferrer" target="_blank">Packagist - Browse packages</a></li>
</ul>
<p>As a PHP developer for sure you are going to use Composer! We are not going to install it directly in <code>/usr/local/bin</code> as this directory is write-protected for the regular system user and we don't ever want to run Composer with <code>sudo</code>. Install it somewhere else and simply symlink it into <code>/usr/local/bin</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">mkdir</span> <span class="token parameter variable">-p</span> /Applications/Tools <span class="token operator">&#x26;&#x26;</span> <span class="token builtin class-name">cd</span> /Applications/Tools/
$ <span class="token function">curl</span> <span class="token parameter variable">-sS</span> https://getcomposer.org/installer <span class="token operator">|</span> php
$ <span class="token function">sudo</span> <span class="token function">ln</span> <span class="token parameter variable">-sf</span> /Applications/Tools/composer.phar /usr/local/bin/composer
</code></pre></div>
<p>Upgrading Composer is terribly easy and does not require superuser rights:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">composer</span> self-update
</code></pre></div>
<h2 id="mysql"><a href="#mysql" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>MySQL</h2>
<p>Download the <strong>"Mac OS X 10.9 (x86, 64-bit), DMG Archive"</strong> from official <a href="https://dev.mysql.com/downloads/mysql/" rel="nofollow noopener noreferrer" target="_blank">dev.mysql.com</a> and install the following packages:</p>
<ul>
<li><strong>mysql-5.6.xx-osx10.9-x86_64.pkg</strong></li>
<li><strong>MySQLStartupItem.pkg</strong></li>
<li><strong>MySQL.prefPane</strong></li>
</ul>
<p>Then, open the pref pane and start the MySQL Server.</p>
<p>Update the path by editing <code>/etc/bashrc</code> (or <code>~/.bash_profile</code>) and add:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token builtin class-name">export</span> <span class="token assign-left variable"><span class="token environment constant">PATH</span></span><span class="token operator">=</span>/usr/local/bin:/usr/local/mysql/bin:<span class="token environment constant">$PATH</span>
</code></pre></div>
<p>at top of file.</p>
<p>Set up MySQL root password:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ mysqladmin <span class="token parameter variable">-u</span> root password <span class="token punctuation">{</span>new-password<span class="token punctuation">}</span>
$ mysqladmin <span class="token parameter variable">-u</span> root -p<span class="token punctuation">{</span>new-password<span class="token punctuation">}</span> <span class="token parameter variable">-h</span> localhost password <span class="token punctuation">{</span>new-password<span class="token punctuation">}</span>
$ mysqladmin <span class="token parameter variable">-u</span> root -p<span class="token punctuation">{</span>new-password<span class="token punctuation">}</span> reload
</code></pre></div>
<p>Clear the history file by typing <code>history -c</code> so that <code>{new-password}</code> isn't in plain text on the disk!</p>
<h2 id="apache"><a href="#apache" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Apache</h2>
<p>OS X Yosemite (10.10) ships with <strong>Apache 2.4.9</strong>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ apachectl <span class="token parameter variable">-v</span>
Server version: Apache/2.4.9 <span class="token punctuation">(</span>Unix<span class="token punctuation">)</span>
Server built:   Sep  <span class="token number">9</span> <span class="token number">2014</span> <span class="token number">14</span>:48:20
</code></pre></div>
<p>This is great - no need for any other Apache installation! We just have to use the command line to start and stop it:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">sudo</span> apachectl stop<span class="token operator">|</span>start<span class="token operator">|</span>restart
</code></pre></div>
<p>Tune main Apache configuration in <code>/etc/apache2/httpd.conf</code>:</p>
<ul>
<li>enable some modules (remove the leading # from these lines)</li>
<li>change the default user Apache is running as to your system user (like this you don't run into any permission trouble in your own projects)</li>
<li>include <code>/etc/apache2/extra/httpd-vhosts.conf</code> virtualhost configuration (see below)</li>
<li>set some environment variables</li>
</ul>
<div class="nuxt-content-highlight"><span class="filename">httpd.conf</span><pre class="line-numbers language-apacheconf"><code><span class="token comment"># required for our PHP-as-CGI setup</span>
<span class="token directive-inline property">LoadModule</span> cgi_module libexec/apache2/mod_cgi.so
<span class="token directive-inline property">LoadModule</span> actions_module libexec/apache2/mod_actions.so
<span class="token comment"># required for rewriting in .htaccess</span>
<span class="token directive-inline property">LoadModule</span> rewrite_module libexec/apache2/mod_rewrite.so
 
<span class="token comment"># (...)</span>
<span class="token directive-inline property">User</span> youruser
<span class="token directive-inline property">Group</span> _www
 
<span class="token comment"># (...)</span>
<span class="token directive-inline property">Include</span> /private/etc/apache2/extra/httpd-vhosts.conf
 
<span class="token comment"># (...)</span>
<span class="token directive-inline property">SetEnv</span> APPLICATION_ENV development
</code></pre></div>
<p>Configure PHP as CGI in <code>/etc/apache2/other/php5.conf</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">other/php5.conf</span><pre class="line-numbers language-apacheconf"><code><span class="token directive-inline property">DirectoryIndex</span> index.php index.html

<span class="token comment"># Homebrew PHP 5.6</span>
<span class="token directive-inline property">ScriptAlias</span> /phpcgi /usr/local/bin/php-cgi
<span class="token directive-inline property">Action</span> php-cgi /phpcgi
<span class="token directive-inline property">AddHandler</span> php-cgi .php
</code></pre></div>
<p>Comment out the examples in <code>/etc/apache2/extra/httpd-vhosts.conf</code> and add the following lines:</p>
<div class="nuxt-content-highlight"><span class="filename">extra/httpd-vhosts.conf</span><pre class="line-numbers language-apacheconf"><code><span class="token comment"># Override the default httpd.conf directives. Make sure to</span>
<span class="token comment"># use 'Require all granted' to prevent 403 Forbidden message.</span>
<span class="token directive-inline property">Options</span> ExecCGI SymLinksIfOwnerMatch Indexes
<span class="token directive-inline property">AllowOverride</span> Indexes AuthConfig Limit FileInfo Options=Indexes,MultiViews
<span class="token directive-inline property">Require</span> all granted

<span class="token comment">#</span>
<span class="token comment"># Use name-based virtual hosting.</span>
<span class="token comment">#</span>
<span class="token directive-inline property">NameVirtualHost</span> *:80

<span class="token comment">#</span>
<span class="token comment"># The first VirtualHost section is used for all requests that do not</span>
<span class="token comment"># match a ServerName or ServerAlias in any block.</span>
<span class="token comment">#</span>
<span class="token directive-inline property">DocumentRoot</span> <span class="token string">"/Library/WebServer/Documents"</span>
<span class="token directive-block tag"><span class="token directive-block tag"><span class="token punctuation">&#x3C;</span>Directory</span><span class="token directive-block-parameter attr-value"> <span class="token string">"/Library/WebServer/Documents"</span></span><span class="token punctuation">></span></span>
    <span class="token directive-inline property">Options</span> Indexes FollowSymLinks MultiViews
    <span class="token directive-inline property">AllowOverride</span> None
    <span class="token directive-inline property">Require</span> all granted
<span class="token directive-block tag"><span class="token directive-block tag"><span class="token punctuation">&#x3C;/</span>Directory</span><span class="token punctuation">></span></span>
</code></pre></div>
<p>You may then add all your VirtualHosts at the bottom of this file, e.g.:</p>
<div class="nuxt-content-highlight"><span class="filename">extra/httpd-vhosts.conf</span><pre class="line-numbers language-apacheconf"><code><span class="token directive-block tag"><span class="token directive-block tag"><span class="token punctuation">&#x3C;</span>VirtualHost</span><span class="token directive-block-parameter attr-value"> *<span class="token punctuation">:</span>80</span><span class="token punctuation">></span></span>
    <span class="token directive-inline property">DocumentRoot</span> <span class="token string">"/path/to/your/project/public"</span>
    <span class="token directive-inline property">ServerName</span> demoproject.test
<span class="token directive-block tag"><span class="token directive-block tag"><span class="token punctuation">&#x3C;/</span>VirtualHost</span><span class="token punctuation">></span></span>
</code></pre></div>
<p>Now, add the project's hostname to <code>/etc/hosts</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>127.0.0.1       demoproject.test
</code></pre></div>
<p>Flush the local DNS cache after every modification in <code>/etc/hosts</code>:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ dscacheutil <span class="token parameter variable">-flushcache</span>
</code></pre></div>
<p>Create a phpinfo.php for testing:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token builtin class-name">echo</span> <span class="token string">"&#x3C;?php phpinfo();"</span> <span class="token operator">></span> /Library/WebServer/Documents/phpinfo.php
</code></pre></div>
<p>Finally, test configuration and restart Apache:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ apachectl configtest
$ <span class="token function">sudo</span> apachectl restart
</code></pre></div>
<p>Testing:</p>
<ul>
<li><a href="http://localhost/phpinfo.php" rel="nofollow noopener noreferrer" target="_blank">http://localhost/phpinfo.php</a></li>
<li><a href="http://demoproject.test" rel="nofollow noopener noreferrer" target="_blank">http://demoproject.test</a></li>
</ul>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="macos" term="macos"/>
        <category label="php" term="php"/>
        <category label="mysql" term="mysql"/>
        <category label="apache" term="apache"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Sendmail-Wrapper for PHP]]></title>
        <id>https://pipo.blog/articles/20140324-php-sendmail-wrapper</id>
        <link href="https://pipo.blog/articles/20140324-php-sendmail-wrapper"/>
        <updated>2014-03-24T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A powerful sendmail wrapper to log and throttle emails sent by PHP.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/sendmail-wrapper.png"></p>
                      <p>Years ago I wrote about the <a href="https://old.iezzi.ch/extensive-sendmail-wrapper-with-sender-throttling/" rel="nofollow noopener noreferrer" target="_blank">extensive sendmail wrapper with sender throttling</a>, a pretty simple Perl script. It reliably provided throttling of the  email volume per day by the sender’s original UID (user id). It also  logged the pathes of scripts that sent emails directly via sendmail  (e.g. via <a href="https://www.php.net/manual/en/function.mail.php" rel="nofollow noopener noreferrer" target="_blank">PHP’s mail() function</a>). The main flaw in the original sendmail wrapper was security, though. As in Linux, every executable script must be readable by the user that  calls it, the throttle table in MySQL was basically open and every  customer could manipulate it. Every customer could raise his own  throttling limit and circumvent it.</p>
<p>Today, I’m publishing my new sendmail-wrapper that is going to fix  all the flaws of the previous version and add some nice extras.
The new sendmail-wrapper is written entirely in PHP and does not require any external libraries. It is a complete rewrite and has pretty much  nothing in common with the old Perl version.</p>
<!--more-->
<ul>
<li><strong><a href="https://github.com/onlime/sendmail-wrapper" rel="nofollow noopener noreferrer" target="_blank">onlime/sendmail-wrapper</a></strong> (Github)</li>
</ul>
<h2 id="features"><a href="#features" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Features</h2>
<ul>
<li>Lets you <strong>monitor any mail traffic from PHP scripts</strong></li>
<li>Allows <strong>throttling (limiting) emails</strong> sent by PHP’s mail() function</li>
<li>Throttle by sent email and/or recipient count per day</li>
<li><strong>Logs both to syslog and database with message metadata</strong></li>
<li>Logs common mail headers like From, To, Cc, Bcc, Subject</li>
<li><strong>Fixes Return-Path header on the fly</strong> for users who did not correctly set it</li>
<li><strong>Highly secured setup</strong>, customers cannot access the logging/throttling database</li>
<li>Standalone PHP application without any external library dependencies</li>
<li>Built for shared webhosting environment where PHP runs as cgi/FastCGI/suPHP</li>
<li><strong>No cronjobs required</strong>, sendmail-wrapper will reset counters automatically every day</li>
</ul>
<h2 id="the-idea-behind-this-secure-setup"><a href="#the-idea-behind-this-secure-setup" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>The idea behind this secure setup</h2>
<p>Logging is done only via syslog, no customer can read or modify  syslog entries. The sendmail-wrapper is split into two components: <code>sendmail-wrapper.php</code> and <code>sendmail-throttle.php</code>. The wrapper component will only do some syslogging and analyze some headers. It will also fix the <code>Return-Path</code> header if applicable as most customers forget to correctly set it in PHP. <code>sendmail-wrapper.php</code> is readable and executable by every customer. This is a strong  requirement as our customers all run their script under their own UID,  PHP runs as cgi/FastCGI/suPHP. The throttle component is where it gets  more interesting. The <code>sendmail-throttle.php</code> script can only be executed by user sendmailwrapper which is an equally unprivileged  system user like every other customer user. The throttling script will  NOT be world readable and it will read it’s configuration from an extra  file <code>config.private.ini</code> which contains the database password. The correct permissions are set by our installation script.</p>
<h2 id="return-path-fixing"><a href="#return-path-fixing" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Return-Path fixing</h2>
<p>Most customers just forget to set a <code>Return-Path</code> when they use PHP’s built-in <a href="https://www.php.net/manual/en/function.mail.php" rel="nofollow noopener noreferrer" target="_blank">mail() function</a>. Even if they try to set it in the <code>$additional_headers</code> variable, it will not get correctly set on most systems. The only way ist to set it via the <code>$additional_parameters</code> variable, using the <code>-f</code> or <code>-r</code> switches. 99% of our customers simply can’t manage this, seriously!
This sendmail-wrapper is extracting the email address from the <code>Return-Path</code> header, and if it doesn’t exist, it will just take the sender’s address (from the <code>From</code> header) as return-path. This is actually one of the most important  features of my sendmail-wrapper, even if it might get overlooked and  you’re installing the wrapper because of it’s logging and throttling  capabilities.</p>
<h2 id="installation"><a href="#installation" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Installation</h2>
<p>(for detailed instructions, please consult the <a href="https://github.com/onlime/sendmail-wrapper" rel="nofollow noopener noreferrer" target="_blank">README on Github</a>)</p>
<p>Clone repository from GitHub:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token builtin class-name">cd</span> /opt/
$ <span class="token function">git</span> clone https://github.com/onlime/sendmail-wrapper.git sendmail-wrapper
</code></pre></div>
<p>Set up system user for sendmail-wrapper:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ adduser <span class="token parameter variable">--system</span> <span class="token parameter variable">--home</span> /no/home --no-create-home <span class="token parameter variable">--uid</span> <span class="token number">6000</span> <span class="token parameter variable">--group</span> --disabled-password --disabled-login sendmailwrapper
$ adduser sendmailwrapper customers
</code></pre></div>
<p>The installer script <code>install.sh</code> will correctly set up permissions and symlink the wrapper scripts:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token builtin class-name">cd</span> /opt/sendmail-wrapper/
$ ./install.sh
</code></pre></div>
<p>Add the following lines to your <code>/etc/sudoers</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">/etc/sudoers</span><pre class="line-numbers language-text"><code>www-data        ALL = (sendmailwrapper) NOPASSWD:/usr/sbin/sendmail-throttle [0-9]*
%customers      ALL = (sendmailwrapper) NOPASSWD:/usr/sbin/sendmail-throttle [0-9]*
</code></pre></div>
<p>Add/modify the following in your php.ini:</p>
<div class="nuxt-content-highlight"><span class="filename">php.ini</span><pre class="line-numbers language-ini"><code><span class="token key attr-name">sendmail_path</span> <span class="token punctuation">=</span> <span class="token value attr-value">/usr/sbin/sendmail-wrapper</span>
<span class="token key attr-name">auto_prepend_file</span> <span class="token punctuation">=</span> <span class="token value attr-value">/var/www/shared/prepend.php</span>
</code></pre></div>
<p>Import the sendmailwrapper database schema:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ mysql <span class="token parameter variable">-u</span> root <span class="token parameter variable">-p</span> <span class="token operator">&#x3C;</span> schema/schema.mysql.sql
</code></pre></div>
<p>Create a MySQL user with the following permissions:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-sql"><code><span class="token keyword">GRANT</span> <span class="token keyword">USAGE</span> <span class="token keyword">ON</span> <span class="token operator">*</span><span class="token punctuation">.</span><span class="token operator">*</span> <span class="token keyword">TO</span> sendmailwrapper<span class="token variable">@'localhost'</span> IDENTIFIED <span class="token keyword">BY</span> <span class="token string">'********'</span><span class="token punctuation">;</span>
<span class="token keyword">GRANT</span> <span class="token keyword">SELECT</span><span class="token punctuation">,</span> <span class="token keyword">INSERT</span><span class="token punctuation">,</span> <span class="token keyword">UPDATE</span> <span class="token keyword">ON</span> sendmailwrapper<span class="token punctuation">.</span>throttle <span class="token keyword">TO</span> sendmailwrapper<span class="token variable">@'localhost'</span><span class="token punctuation">;</span>
<span class="token keyword">GRANT</span> <span class="token keyword">INSERT</span> <span class="token keyword">ON</span> sendmailwrapper<span class="token punctuation">.</span>messages <span class="token keyword">TO</span> sendmailwrapper<span class="token variable">@'localhost'</span><span class="token punctuation">;</span>
</code></pre></div>
<h2 id="configuration"><a href="#configuration" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Configuration</h2>
<p>Default configuration can be found in <code>config.ini</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">config.ini</span><pre class="line-numbers language-ini"><code><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">global</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">defaultTZ</span> <span class="token punctuation">=</span> <span class="token value attr-value">Europe/Zurich</span>
<span class="token key attr-name">adminTo</span> <span class="token punctuation">=</span> <span class="token value attr-value">hostmaster@example.com</span>
<span class="token key attr-name">adminFrom</span> <span class="token punctuation">=</span> <span class="token value attr-value">hostmaster@example.com</span>

<span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">wrapper</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">sendmailCmd</span> <span class="token punctuation">=</span> <span class="token value attr-value">"<span class="token inner-value">/usr/sbin/sendmail -t -i</span>"</span>
<span class="token key attr-name">throttleCmd</span> <span class="token punctuation">=</span><span class="token value attr-value">"<span class="token inner-value">sudo -u sendmailwrapper /usr/sbin/sendmail-throttle</span>"</span>
<span class="token key attr-name">throttleOn</span> <span class="token punctuation">=</span> <span class="token value attr-value">true</span>
<span class="token key attr-name">defaultHost</span> <span class="token punctuation">=</span> <span class="token value attr-value">"<span class="token inner-value">example.com</span>"</span>
<span class="token key attr-name">syslogPrefix</span> <span class="token punctuation">=</span> <span class="token value attr-value">sendmail-wrapper-php</span>
<span class="token key attr-name">xHeaderPrefix</span> <span class="token punctuation">=</span> <span class="token value attr-value">"<span class="token inner-value">X-Example-</span>"</span>

<span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">throttle</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">countMax</span> <span class="token punctuation">=</span> <span class="token value attr-value">1000</span>
<span class="token key attr-name">rcptMax</span> <span class="token punctuation">=</span> <span class="token value attr-value">1000</span>
<span class="token key attr-name">syslogPrefix</span> <span class="token punctuation">=</span> <span class="token value attr-value">sendmail-throttle-php</span>
<span class="token key attr-name">adminSubject</span> <span class="token punctuation">=</span> <span class="token value attr-value">"<span class="token inner-value">Sendmail limit exceeded</span>"</span>

<span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">db</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">dsn</span> <span class="token punctuation">=</span> <span class="token value attr-value">"<span class="token inner-value">mysql:host=localhost;dbname=sendmailwrapper</span>"</span>
<span class="token key attr-name">user</span> <span class="token punctuation">=</span> <span class="token value attr-value">sendmailwrapper</span>
<span class="token key attr-name">pass</span> <span class="token punctuation">=</span> <span class="token value attr-value">"<span class="token inner-value">xxxxxxxxxxxxxxxxxxxxx</span>"</span>
</code></pre></div>
<p><strong>You should not change any of the above values.</strong> Create your own <code>config.local.ini</code> instead to overwrite some values, e.g.:</p>
<div class="nuxt-content-highlight"><span class="filename">config.local.ini</span><pre class="line-numbers language-ini"><code><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">global</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">adminTo</span> <span class="token punctuation">=</span> <span class="token value attr-value">hostmaster@mydomain.com</span>
<span class="token key attr-name">adminFrom</span> <span class="token punctuation">=</span> <span class="token value attr-value">hostmaster@mydomain.com</span>

<span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">wrapper</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">defaultHost</span> <span class="token punctuation">=</span> <span class="token value attr-value">"<span class="token inner-value">mydomain.com</span>"</span>
<span class="token key attr-name">xHeaderPrefix</span> <span class="token punctuation">=</span> <span class="token value attr-value">"<span class="token inner-value">X-MyCompany-</span>"</span>
</code></pre></div>
<p>Never put your database password in any of the above configuration files. Use another configuration file called <code>config.private.ini</code> instead, e.g.:</p>
<div class="nuxt-content-highlight"><span class="filename">config.private.ini</span><pre class="line-numbers language-ini"><code><span class="token section"><span class="token punctuation">[</span><span class="token section-name selector">db</span><span class="token punctuation">]</span></span>
<span class="token key attr-name">pass</span> <span class="token punctuation">=</span> <span class="token value attr-value">"<span class="token inner-value">mySuper-SecurePassword/826.4287+foo</span>"</span>
</code></pre></div>
<p>Sample syslog output:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>Mar 24 11:50:21 web1 php: sendmail-throttle-php: user=web4111 (4111:4111), rcpts=1, status=0, command=/usr/sbin/sendmail-throttle 1, count_max=1000, count_cur=3, count_tot=9, rcpt_max=1000, rcpt_cur=3, rcpt_tot=9
Mar 24 11:50:21 web1 php: sendmail-wrapper-php: uid=web4111#012, msgid=20140324115021.7xp40a@example.com, from=foo@example.com, to="bar@example.com", cc="", bcc="", subject="Just testing", site=www.example.com, client=8.8.1.1, script=/var/www/example/public_html/www/index.php, throttleStatus=0
</code></pre></div>
<p>MySQL database <code>sendmailwrapper</code>:</p>
<p>You may access both MySQL tables directly: <code>throttle</code> to tweak each system user’s limits (they will be persistent! Only counters are gettingn reset every day), <code>messages</code> for detailed message header reviewing or for statistical purposes.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="php" term="php"/>
        <category label="email" term="email"/>
        <category label="security" term="security"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Webapp-Scanner]]></title>
        <id>https://pipo.blog/articles/20140323-webapp-scanner</id>
        <link href="https://pipo.blog/articles/20140323-webapp-scanner"/>
        <updated>2014-03-23T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[A webapplication scanner that detects all outdated webapps on a server.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/webapp-scanner.png"></p>
                      <p>Webhosting customers are messies, at least some of them - or (sadly, that's the truth) the bigger part of them. Some people still think they can run the same blog software or CMS for years without ever caring about upgrading. I tell my customers over and over how important it is, to keep their website up-to-date and don't let any outdated code lying around. Still, as long as their website doesn't get hacked or defaced, they don't really seem to care.</p>
<p>If you're in the same situation as me and you are providing webhosting services to your friends or customers, read on.</p>
<!--more-->
<h2 id="purpose"><a href="#purpose" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Purpose</h2>
<p>The <a href="https://github.com/onlime/webapp-scanner" rel="nofollow noopener noreferrer" target="_blank">onlime/webapp-scanner</a> script scans the user directories and reports the installed CMS packages and what version it is running. It can be set to report just outdated packages and scan specific users. It has the ability to search for common versions of the following packages:</p>
<ul>
<li><a href="https://wordpress.org/" rel="nofollow noopener noreferrer" target="_blank">WordPress</a></li>
<li><a href="https://typo3.org/" rel="nofollow noopener noreferrer" target="_blank">Typo3</a></li>
<li><a href="https://www.joomla.org/" rel="nofollow noopener noreferrer" target="_blank">Joomla</a></li>
<li><a href="https://drupal.org" rel="nofollow noopener noreferrer" target="_blank">Drupal</a></li>
<li><a href="https://e107.org/" rel="nofollow noopener noreferrer" target="_blank">e107</a></li>
<li><a href="https://www.mamboserver.com/" rel="nofollow noopener noreferrer" target="_blank">Mambo</a></li>
<li><a href="https://www.mediawiki.org" rel="nofollow noopener noreferrer" target="_blank">MediaWiki</a></li>
<li><a href="https://openx.com/" rel="nofollow noopener noreferrer" target="_blank">OpenX</a></li>
<li><a href="https://www.oscommerce.com/" rel="nofollow noopener noreferrer" target="_blank">osCommerce2</a></li>
<li><a href="https://www.phpbb.com/" rel="nofollow noopener noreferrer" target="_blank">phpBB3</a></li>
<li><a href="https://piwigo.org/" rel="nofollow noopener noreferrer" target="_blank">Piwigo</a></li>
<li><a href="https://www.redmine.org/" rel="nofollow noopener noreferrer" target="_blank">Redmine</a></li>
<li><a href="https://www.x-cart.com/" rel="nofollow noopener noreferrer" target="_blank">X-Cart</a></li>
<li><a href="https://www.xoops.org/" rel="nofollow noopener noreferrer" target="_blank">XOOPS</a></li>
<li><a href="https://www.zen-cart.com/" rel="nofollow noopener noreferrer" target="_blank">ZenCart</a></li>
</ul>
<p>The script is based on <a href="http://g33kinfo.com/info/archives/5981" rel="nofollow noopener noreferrer" target="_blank">Software Version finder</a> by James Dooley. I have extended it to also scan for Typo3 installations and provide some more output.</p>
<h2 id="installation"><a href="#installation" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Installation</h2>
<div class="nuxt-content-highlight"><pre class="language-bash line-numbers"><code>$ <span class="token builtin class-name">cd</span> /opt/
$ <span class="token function">git</span> clone https://github.com/onlime/webapp-scanner.git webapp-scanner
$ <span class="token builtin class-name">cd</span> webapp-scanner
$ <span class="token function">chmod</span> +x webapp-scanner.sh
</code></pre></div>
<p>That's it. If you wish to scan all your customers homes, make sure you run this as root. It won't alter any files on your system, simply scan it.</p>
<p>For more convenience, add the following alias to your <code>~/.bashrc</code> or globally to <code>/etc/profile</code>:</p>
<div class="nuxt-content-highlight"><pre class="language-bash line-numbers"><code><span class="token builtin class-name">alias</span> <span class="token assign-left variable">webappscanner</span><span class="token operator">=</span><span class="token string">'/opt/webapp-scanner/webapp-scanner.sh'</span>
</code></pre></div>
<h2 id="usage"><a href="#usage" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Usage</h2>
<p>For usage please consult the <a href="https://github.com/onlime/webapp-scanner" rel="nofollow noopener noreferrer" target="_blank">README on Github</a>.</p>
<p><strong>Sample output:</strong></p>
<p>Scanning the whole /var/www recursively for any webapps (this will also report webapps that are up-to-date):</p>
<div class="nuxt-content-highlight"><pre class="language-bash line-numbers"><code>$ webappscanner <span class="token parameter variable">--directory</span> /var/www/
Typo3_4.5   <span class="token number">4.5</span>.25      <span class="token number">4.5</span>.32      /var/www/web123/public_html/www/
Typo3_4.5   <span class="token number">4.5</span>.30      <span class="token number">4.5</span>.32      /var/www/web345/public_html/www/
Typo3_4.7   <span class="token number">4.7</span>.17      <span class="token number">4.7</span>.17      /var/www/web678/public_html/www/
Joomla_1.5  <span class="token number">1.5</span>.15      <span class="token number">1.5</span>.999     /var/www/web111/public_html/old/
Joomla_1.5  <span class="token number">1.5</span>.26      <span class="token number">1.5</span>.999     /var/www/web222/public_html/www/
Joomla_3.2  <span class="token number">3.1</span>.5       <span class="token number">3.2</span>.2       /var/www/web101/public_html/www/
Joomla_3.2  <span class="token number">3.0</span>.3       <span class="token number">3.2</span>.2       /var/www/web202/public_html/www/
Joomla_3.2  <span class="token number">3.2</span>.2       <span class="token number">3.2</span>.2       /var/www/web303/public_html/relaunch/
WordPress   <span class="token number">3.8</span>         <span class="token number">3.8</span>.1       /var/www/web110/public_html/www/
WordPress   <span class="token number">3.8</span>.1       <span class="token number">3.8</span>.1       /var/www/web120/public_html/www/
</code></pre></div>
<p><strong>CSV output:</strong></p>
<p>In addition to the default output, we will get an additional version status code:</p>
<ul>
<li>0: <code>OK</code> (version equals or is greater than current version)</li>
<li>1: <code>WARNING</code> (version is greater than minimal version but not >= current version)</li>
<li>2: <code>CRITICAL</code> (version is lower than minimal version)</li>
</ul>
<p>sample output:</p>
<div class="nuxt-content-highlight"><pre class="language-bash line-numbers"><code>$ webappscanner <span class="token parameter variable">--directory</span> /var/www/ <span class="token parameter variable">--csv</span>
Typo3_4.5,4.5.25,4.5.32,1,/var/www/web123/public_html/www/
Typo3_4.5,4.5.30,4.5.32,1,/var/www/web345/public_html/www/
Typo3_4.7,4.7.17,4.7.17,0,/var/www/web678/public_html/www/
Joomla_1.5,1.5.15,1.5.999,1,/var/www/web111/public_html/old/
Joomla_1.5,1.5.26,1.5.999,1,/var/www/web222/public_html/www/
Joomla_3.2,3.1.5,3.2.2,2,/var/www/web101/public_html/www/
Joomla_3.2,3.0.3,3.2.2,2,/var/www/web202/public_html/www/
Joomla_3.2,3.2.2,3.2.2,0,/var/www/web303/public_html/relaunch/
WordPress,3.8,3.8.1,1,/var/www/web110/public_html/www/
WordPress,3.8.1,3.8.1,0,/var/www/web120/public_html/www/
</code></pre></div>
<p>With the CSV output you are able to use our webapp-scanner in your own application. You could e.g. warn your customers on a weekly basis about their outdated web applications. As a responsible web hosting provider, you should force them earlier or later to upgrade their installations to the latest versions. Also, you should provide them with HOWTO's, instructions, or personal support to ease the upgrade process of their web applications.</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="security" term="security"/>
        <category label="bash" term="bash"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Process hiding - hidepid capabilities of procfs]]></title>
        <id>https://pipo.blog/articles/20130930-hidepid-process-hiding</id>
        <link href="https://pipo.blog/articles/20130930-hidepid-process-hiding"/>
        <updated>2013-09-30T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[Hiding processes for other users using hidepid capabilities of procfs.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/htop-hidepid.png"></p>
                      <p>Five years ago I wrote about kernel based process hiding in Linux (see my old blog posts: <a href="https://old.iezzi.ch/simple-process-hiding-kernel-patch/" rel="nofollow noopener noreferrer" target="_blank">Simple process hiding kernel patch</a>, <a href="https://old.iezzi.ch/process-hiding-kernel-patch/" rel="nofollow noopener noreferrer" target="_blank">Process hiding Kernel patch for 2.6.24.x</a>, <a href="https://old.iezzi.ch/rsbac-kernel-based-process-hiding/" rel="nofollow noopener noreferrer" target="_blank">RSBAC – Kernel based process hiding</a>). It got time to continue the story and finally present you a real solution without the hassle of a self-compiled kernel.</p>
<p><strong>How can I prevent users from seeing processes that do not belong to them?</strong></p>
<p>In January 2012, Vasiliy Kulikov came up with a <a href="https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=0499680a42141d86417a8fbaa8c8db806bea1201" rel="nofollow noopener noreferrer" target="_blank">kernel patch</a> that solved the problem nicely by adding a <code>hidepid</code> mount option for procfs. The patch landed in Linux kernel 3.3.</p>
<p>In the meantime, this patch luckily also landed in the 3.2 kernel of <strong>Debian Wheezy</strong> (see backport request in <a href="http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=669028" rel="nofollow noopener noreferrer" target="_blank">Debian bug report #669028</a>). This feature has been also pushed back into the kernel of <strong>Red Hat Enterprise Linux 6.3</strong> (see <a href="https://access.redhat.com/site/documentation/en-US/Red_Hat_Enterprise_Linux/6/html/6.3_Release_Notes/kernel.html" rel="nofollow noopener noreferrer" target="_blank">RHEL 6.3 Release Notes</a>), and from there to <strong>CentOS 6.3</strong> and <strong>Scientific Linux 6.3</strong>. Recently, this feature was even backported to the 2.6.18 kernel in <strong>RHEL 5.9</strong>.</p>
<!--more-->
<p>As <a href="https://proxmox.com/en/proxmox-ve" rel="nofollow noopener noreferrer" target="_blank">Proxmox VE</a> currently runs on a RHEL based 2.6.32 kernel, it's also supported in my favorite OpenVZ/KVM virtualization platform. Great!</p>
<blockquote>
<p><code>hidepid=0</code> (default) means the current behaviour - anybody may read all
world-readable /proc/PID/* files.</p>
<p><code>hidepid=1</code> means users may not access any /proc/PID/ directories, but their
own. Sensitive files like cmdline, io, sched*, status, wchan are now
protected against other users. As permission checking done in
proc_pid_permission() and files' permissions are left untouched,
programs expecting specific files' permissions are not confused.</p>
<p><code>hidepid=2</code> means hidepid=1 plus all /proc/PID/ will be invisible to
other users. It doesn't mean that it hides a fact whether a process
exists (it can be learned by other means, e.g. by sending signals), but
it hides process' euid and egid. It greatly compicates intruder's task of
gathering info about running processes, whether some daemon runs with
elevated privileges, whether other user runs some sensitive program,
whether other users run any program at all, etc.</p>
</blockquote>
<p>On a shared server I strongly recommend to enable the <code>hidepid=2</code> procfs mount option. This can be done at runtime by remounting procfs:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">mount</span> <span class="token operator">|</span> <span class="token function">grep</span> ^proc
proc on /proc <span class="token builtin class-name">type</span> proc <span class="token punctuation">(</span>rw,relatime<span class="token punctuation">)</span>

$ <span class="token function">mount</span> <span class="token parameter variable">-o</span> remount,hidepid<span class="token operator">=</span><span class="token number">2</span> /proc

$ <span class="token function">mount</span> <span class="token operator">|</span> <span class="token function">grep</span> ^proc
proc on /proc <span class="token builtin class-name">type</span> proc <span class="token punctuation">(</span>rw,relatime,hidepid<span class="token operator">=</span><span class="token number">2</span><span class="token punctuation">)</span>
</code></pre></div>
<p>You may also add this mount option directly to <code>/etc/fstab</code> in order to make it persistent:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-text"><code>proc            /proc           proc    defaults,hidepid=2        0       0
</code></pre></div>
<p>A regular system user will then only see his own processes, e.g.:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>testuser@web:~$ <span class="token function">ps</span>
  PID TTY          TIME CMD
<span class="token number">17486</span> pts/0    00:00:00 <span class="token function">bash</span>
<span class="token number">24806</span> pts/0    00:00:00 <span class="token function">ps</span>
</code></pre></div>
<p>This also works for other commands like <strong>pstree</strong>, <strong>top</strong>, <strong>htop</strong>,...</p>
<p>To get process hiding working inside a Proxmox VE container, simply remount procfs inside the VE, e.g. by the following init script ''/etc/init.d/system-hardening'':</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code><span class="token shebang important">#!/bin/bash</span>

<span class="token comment">### BEGIN INIT INFO</span>
<span class="token comment"># Provides:          system-hardening</span>
<span class="token comment"># Required-Start:    $all</span>
<span class="token comment"># Required-Stop:     $local_fs $remote_fs $syslog $named $network</span>
<span class="token comment"># Should-Start:</span>
<span class="token comment"># Should-Stop:</span>
<span class="token comment"># Default-Start:     2 3 4 5</span>
<span class="token comment"># Default-Stop:      0 1 6</span>
<span class="token comment"># Short-Description: Run system-hardening startup actions</span>
<span class="token comment"># Description:       Run system-hardening startup actions</span>
<span class="token comment">### END INIT INFO</span>

<span class="token builtin class-name">.</span> /lib/lsb/init-functions

log_daemon_msg <span class="token string">"Added proc mount option (hidepid=2)"</span>
<span class="token function">mount</span> <span class="token parameter variable">-o</span> remount,hidepid<span class="token operator">=</span><span class="token number">2</span> /proc
log_end_msg <span class="token variable">$?</span>
</code></pre></div>
<p>Install the init script:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ insserv system-hardening
</code></pre></div>
<p>links/credits to:</p>
<ul>
<li><a href="http://debblog.philkern.de/2012/05/lazyweb-question-how-to-avoid-leaking.html" rel="nofollow noopener noreferrer" target="_blank">Lazyweb question: How to avoid leaking process info? (by Philip Kern)</a></li>
<li><a href="http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=669028" rel="nofollow noopener noreferrer" target="_blank">Debian bug report #669028: backport new procfs hidepid option into 3.2</a></li>
<li><a href="https://git.kernel.org/cgit/linux/kernel/git/torvalds/linux.git/commit/?id=0499680a42141d86417a8fbaa8c8db806bea1201" rel="nofollow noopener noreferrer" target="_blank">kernel patch by Vasiliy Kulikov</a></li>
<li><a href="http://blog.witalis.net/?p=882" rel="nofollow noopener noreferrer" target="_blank">hidepid capabilities of procfs (by Witold Duranek)</a></li>
</ul>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="security" term="security"/>
    </entry>
    <entry>
        <title type="html"><![CDATA[Proxmox VE Restricting Web UI access]]></title>
        <id>https://pipo.blog/articles/20130918-pve-restrict-webui-access</id>
        <link href="https://pipo.blog/articles/20130918-pve-restrict-webui-access"/>
        <updated>2013-09-18T00:00:00.000Z</updated>
        <summary type="html"><![CDATA[How to password protect the web UI of Proxmox VE.]]></summary>
        <content type="html"><![CDATA[
                      <p><img alt="Cover image" src="https://pipo.blog/images/covers/proxmoxve-webui.png"></p>
                      <p>With the release of <strong><a href="https://www.proxmox.com/de/news/pressemitteilungen/proxmox-ve-3-0-veroeffentlicht" rel="nofollow noopener noreferrer" target="_blank">Proxmox VE 3.0</a></strong> back in May 2013, the Proxmox VE web interface does no longer require Apache. Instead, they're using now a new event driven API server called <strong>pveproxy</strong>. That was actually a great step ahead, as we all know Apache get's bulkier every day and the new pveproxy is a much more lightweight solution. But the question arose: <strong>How do I protect my Proxmox VE WebUI with basic user authentication?</strong></p>
<p>Basically, we do not trust any web application out there so we better double protect the whole WebUI with plain old basic auth - previously done in Apache by .htaccess.</p>
<!--more-->
<h2 id="the-main-idea"><a href="#the-main-idea" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>The main idea</h2>
<ul>
<li><strong>Restrict access to the pveproxy (= Web UI) to localhost</strong></li>
<li>Install a local <strong>Nginx web proxy server</strong> that forwards requests from port 443 to pveproxy's port 8006 and restrict access to it using HTTP BASIC AUTH</li>
</ul>
<h2 id="restrict-access-to-pveproxy"><a href="#restrict-access-to-pveproxy" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Restrict access to pveproxy</h2>
<p>Create a new file <code>/etc/default/pveproxy</code> with the following content:</p>
<div class="nuxt-content-highlight"><span class="filename">pveproxy</span><pre class="line-numbers language-ini"><code><span class="token key attr-name">ALLOW_FROM</span><span class="token punctuation">=</span><span class="token value attr-value">"<span class="token inner-value">127.0.0.1</span>"</span>
<span class="token key attr-name">DENY_FROM</span><span class="token punctuation">=</span><span class="token value attr-value">"<span class="token inner-value">all</span>"</span>
<span class="token key attr-name">POLICY</span><span class="token punctuation">=</span><span class="token value attr-value">"<span class="token inner-value">allow</span>"</span>
</code></pre></div>
<p>Restart pveproxy for the changes to take effect:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ /etc/init.d/pveproxy restart
</code></pre></div>
<h2 id="nginx-web-proxy-server"><a href="#nginx-web-proxy-server" aria-hidden="true" tabindex="-1"><span class="icon icon-link"></span></a>Nginx web proxy server</h2>
<p>Install nginx-light (the lightweight package of Nginx is sufficient):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">apt-get</span> <span class="token function">install</span> nginx-light
</code></pre></div>
<p>The following packages will be installed: <code>nginx-common nginx-light</code></p>
<p>Now, copy over your signed SSL certificate to <code>/etc/nginx/conf.d</code>, in case you already have one. You might as well create a self-signed SSL certificate and SSL certificate key, e.g. (validity of 10 years!):</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token builtin class-name">cd</span> /etc/nginx/conf.d/
$ openssl genrsa <span class="token parameter variable">-out</span> server.key <span class="token number">2048</span>
$ openssl req <span class="token parameter variable">-new</span> <span class="token parameter variable">-key</span> server.key <span class="token parameter variable">-out</span> server.csr
$ openssl x509 <span class="token parameter variable">-req</span> <span class="token parameter variable">-days</span> <span class="token number">3650</span> <span class="token parameter variable">-in</span> server.csr <span class="token parameter variable">-signkey</span> server.key <span class="token parameter variable">-out</span> server.crt
</code></pre></div>
<p>Create <code>/etc/nginx/sites-available/pveproxy</code>:</p>
<div class="nuxt-content-highlight"><span class="filename">sites-available/pveproxy</span><pre class="line-numbers language-nginx"><code><span class="token directive"><span class="token keyword">server</span></span> <span class="token punctuation">{</span>
        <span class="token directive"><span class="token keyword">listen</span>          <span class="token number">80</span></span><span class="token punctuation">;</span>
        <span class="token directive"><span class="token keyword">server_name</span>     hn*.example.com</span><span class="token punctuation">;</span>
        <span class="token directive"><span class="token keyword">rewrite</span>         ^ https://<span class="token variable">$hostname</span>.example.com<span class="token variable">$request_uri?</span> permanent</span><span class="token punctuation">;</span>
<span class="token punctuation">}</span>

<span class="token directive"><span class="token keyword">server</span></span> <span class="token punctuation">{</span>
        <span class="token directive"><span class="token keyword">listen</span>                  <span class="token number">443</span> ssl</span><span class="token punctuation">;</span>
        <span class="token directive"><span class="token keyword">server_name</span>             hn*.example.com</span><span class="token punctuation">;</span>
        <span class="token directive"><span class="token keyword">ssl_certificate</span>         /etc/nginx/conf.d/server.crt</span><span class="token punctuation">;</span>
        <span class="token directive"><span class="token keyword">ssl_certificate_key</span>     /etc/nginx/conf.d/server.key</span><span class="token punctuation">;</span>
        <span class="token directive"><span class="token keyword">auth_basic</span>              <span class="token string">"Restricted"</span></span><span class="token punctuation">;</span>
        <span class="token directive"><span class="token keyword">auth_basic_user_file</span>    htpasswd</span><span class="token punctuation">;</span>
        <span class="token directive"><span class="token keyword">location</span> /</span> <span class="token punctuation">{</span> <span class="token directive"><span class="token keyword">proxy_pass</span> https://127.0.0.1:8006</span><span class="token punctuation">;</span> <span class="token punctuation">}</span>
<span class="token punctuation">}</span>
</code></pre></div>
<p>Disable the default site and enable pveproxy:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ <span class="token function">rm</span> <span class="token parameter variable">-f</span> /etc/nginx/sites-enabled/default
$ <span class="token function">ln</span> <span class="token parameter variable">-sf</span> /etc/nginx/sites-available/pveproxy /etc/nginx/sites-enabled/
</code></pre></div>
<p>For details, check <a href="http://nginx.org/en/docs/http/ngx_http_ssl_module.html" rel="nofollow noopener noreferrer" target="_blank">NGINX ngx_http_ssl_module</a>. Note that the certificates could also be placed in another directory (adjust /etc/nginx/nginx.conf accordingly).</p>
<p>Create the htpasswd file in <code>/etc/nginx/htpasswd</code>
I'd recommend to simply create it on another host where you have Apache installed. But in case you have no such tools at hand, check the <a href="https://www.nginx.com/resources/wiki/community/faq/#how-do-i-generate-an-htpasswd-file-without-having-apache-tools-installed" rel="nofollow noopener noreferrer" target="_blank">FAQ: How do I generate an .htpasswd file without having Apache tools installed?</a></p>
<p>Restart Nginx:</p>
<div class="nuxt-content-highlight"><pre class="line-numbers language-bash"><code>$ /etc/init.d/nginx restart
</code></pre></div>
<p>Done! You may now access the Proxmox VE Web UI directly via <code>HTTPS</code> - no separate port required as we are using the standard SSL port 443 for our NGINX proxy.</p>
<p>Credits to: <a href="https://printscreen.ch/" rel="nofollow noopener noreferrer" target="_blank">Printscreen GmbH, Daniel Mettler</a> - Thanks for helping me out with NGINX!</p>
                    ]]></content>
        <author>
            <name>Philip Iezzi</name>
            <email>blog@onlime.ch</email>
            <uri>https://pipo.blog/</uri>
        </author>
        <category label="sysadmin" term="sysadmin"/>
        <category label="linux" term="linux"/>
        <category label="proxmoxve" term="proxmoxve"/>
        <category label="security" term="security"/>
    </entry>
</feed>