It is the year 2021, and I use an Albert Heijn shopping bag, one of the old ones obtained years earlier, when the ultimate nightmare occurs: upon leaving the supermarket I hear a ripping sound and the side of the bag tears open.

I post about the disaster and my distress on social media, but I am actually amused: I’d normally have no qualms in just ditching the bag and using something else. Somehow I’m attached to this awfully ugly blue bag.

My friends Ton from the Netherlands and Nicolai from Berlin promise to come to my aid and both send me a few Albert Heijn bags. I have never had the heart to confess to them that I didn’t actually use the bags; they had to pay for them and the postage to me. I still have them in the cellar, unused.

Meanwhile, you see, ah had redesigned their shopping bags. It becomes taller and narrower, and it looks as though it won’t take the loads I’m used to transporting: groceries for a week, possibly with tins and bottles. Certainly many kilograms of weight per load.

I see two possible solutions: I starve from no longer being able to shop for groceries or patch the bag, and thanks to the availability of a strip from a roll of duct tape I decide on the latter. It’s the strip along the height of the bag seen here on the bottom of the photo.

a tape-patched albert heijn shopping bag

That patch held for almost five years!

I went grocery shopping today again, and added lots of heavy items to the bag: a cauliflower and other vegetables, a kg of meat, three 1.5L bottles of cleaning material, four tins of beer, a bottle of olive oil, several jars of capers, two tins of tomato polpo and three of tuna fish, etc. etc. and I suddenly feel more than hear a tear in the handles.

I make it to the self-checkout, and carefully repack the Albert Heijn bag. I gingerly carry it to the car. Upon arriving home I apply several strips of duct tape along the edges of the carrying handles, hoping to give the bag another breath of life.

Maybe for another five years? I cross my fingers.

shopping :: 03 Jan 2026 :: e-mail

I need to generate TSIG key files for a DNS server using Ansible, and while I could use something like the community.general.random_string lookup, due to Ansible’s lazy evaluation of lookups, using that in a vars would cause the lookup to be re-evaluated upon use meaning I cannot actually use it more than once in a playbook.

So I create a lookup plugin which generates BIND-compatible TSIG key files and returns their individual components as a dict.

To verify the key files are compatible, I generate a TSIG key using BIND’s utility:

$ tsig-keygen blog-demo > bind.key

$ cat bind.key
key "blog-demo" {
	algorithm hmac-sha256;
	secret "/akt2yQEYmZ1L+yao06mkRuS31m4/Dn994KMDq6PtHE=";
};

The short playbook will demo the idempotency of the lookup:

- hosts: localhost
  gather_facts: false
  vars:
    tk: "{{ lookup('tsig_key', 'tsig.f2', tsig_name='example-net') }}"
  tasks:
    - debug: msg="{{ lookup('tsig_key', 'bind.key') }}"

    - debug: msg="{{ tk }}"
$ ansible-playbook gen-key.yml
PLAY [localhost] ***************************************************************************

TASK [debug] *******************************************************************************
ok: [localhost] => {
    "msg": {
        "tsig_algo": "hmac-sha256",
        "tsig_file": "bind.key",
        "tsig_name": "blog-demo",
        "tsig_secret": "/akt2yQEYmZ1L+yao06mkRuS31m4/Dn994KMDq6PtHE="
    }
}

TASK [debug] *******************************************************************************
ok: [localhost] => {
    "msg": {
        "tsig_algo": "hmac-sha256",
        "tsig_file": "tsig.f2",
        "tsig_name": "example-net",
        "tsig_secret": "OV/xk8C2JeqZBcdMeS/BUFKscdDFDkipGiFy6bGuc24="
    }
}
$ cat tsig.f2
# hmac-sha256:example-net:OV/xk8C2JeqZBcdMeS/BUFKscdDFDkipGiFy6bGuc24=

key "example-net" {
    algorithm hmac-sha256;
    secret "OV/xk8C2JeqZBcdMeS/BUFKscdDFDkipGiFy6bGuc24=";
};

The files generated by the tsig_key lookup include a comment in the key file containing colon-separated algorithm, key name, and key secret, in case I ever want to automatically extract that and use in dig(1)’s -y option, as opposed to the specifying the key file proper with -k.

ansible and DNS :: 02 Jan 2026 :: e-mail

I’ve been looking a bit more closely at Semaphore UI and was curious how secrets I can configure for it are stored. There are two distinct kinds of secrets as far as I’ve been able to ascertain, and they’re in either variable groups or in one or more key stores (one key store for the CE edition, more than one for the Pro).

I create three such secret values, using the UI:

  1. a secret in a variable group; secret environment variables in a variable group are also encrypted and work like “normal” secret variables: a secret in a variable group

  2. an SSH key in a key store into which I paste the SSH key’s passphrase and the private key, generated with ssh-keygen -C DemoKey -t ed25519 -f sema1: an ssh key in a key store

  3. a login/password combination a playbook can, say, use to login to “something”: a login/password in a key store

These values are then encrypted at rest with AES into the database, which is what the documentation states, and an SQL select confirms base64-encoded data in the secret column (id 1 was placed there by the setup routine):

sqlite> SELECT id, name, type, SUBSTR(secret, 1, 25) AS secret FROM access_key;
┌────┬──────────────┬────────────────┬───────────────────────────┐
│ id │     name     │      type      │          secret           │
├────┼──────────────┼────────────────┼───────────────────────────┤
│ 1  │ None         │ none           │                           │
│ 2  │ mug          │ string         │ cIRZIUUaFCxajOak6I51agLyq │
│ 3  │ dev-hosts    │ ssh            │ EzLAwD/CB7y23x/XABElWrF9g │
│ 4  │ router-creds │ login_password │ 6in0q49PmW1XrhQ33wTEO/GuG │
└────┴──────────────┴────────────────┴───────────────────────────┘

I spend a futile 20 minutes trying to decrypt the binary data (i.e. after base64 decoding) using openssl enc -d, futile because I only find out later that the nonce is actually in the first 12 bytes of the encrypted blob. I also spend an impossible amount of time consulting a robot which insists on hallucinating and leading me up the garden path until I’m breathless.

And then, finally, I read in enc’s documentation:

This command does not support authenticated encryption modes like CCM and GCM, and will not support such modes in the future.

Case closed.

So I go looking for Semaphore UI’s source code and find what I need in services/server/access_key_serializer_local.go. I adapt (newspeak for copy/paste) what I need into a jp.go which then uses the access_key_encryption created into Semaphore UI’s config.json to decrypt the values after decoding from base64.

The config.json I have here (keep your hats on, please, this is a throwaway install so I can actually show you the real strings) is

{
  "sqlite": {
    "host": "/tmp/semaphore/jp/database.sqlite"
  },
  "dialect": "sqlite",
  "tmp_path": "/var/folders/zw/gxqp7b7j1tb27n275tjt7c180000gn/T/semaphore",
  "cookie_hash": "a/AVelmRTIZ264M3/+aU/Puv7QAG/qucxA25i3pRqJo=",
  "cookie_encryption": "bWyYkEt0zoFkuFY2/HrO7nrMoXTZvVblx1Bewc17Szs=",
  "access_key_encryption": "Q36I3zFl+CQz8F1mkeT0d8pbsQC0qksfqvWI1vLYDnU="
}

So, if I copy/paste the base64 strings from the database into my jp.go I actually see their decrypted values as Semaphore will later use them.

$ go run jp.go
Full of coffee, please

In order of the screenshots above, I obtain the following decrypted strings:

  1. Full of coffee, please
  2. {"login":"ansible","passphrase":"MyDearAuntSally","private_key":"-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAACmFlczI1Ni1jdHIAAAAGYmNyeXB0AAAAGAAAABCdG7azNe\no+VS4p9MB9a2s0AAAAGAAAAAEAAAAzAAAAC3NzaC1lZDI1NTE5AAAAIOBEyAbchIujQbVt\nRB3VCq7ROGSmE87e4/7mZQa3FR/ZAAAAkC4lJZPQ7d6xUylLzznS12TqjPKj7ta/cTMZ5P\n31zgl4QCOO+Nl5hYrsJ+AvbTIepGAd7eymqXaJr2eDu/kntLlE3uDamRL5midyw2dwak04\n9zhkr9ZeYxeA1ggGYilY/Ise2z5j4bG+2Zzo07kkTjVxU45HXH+1VFyCRTK80Lu67Csz94\nLXq5dyDBSM8IZXOA==\n-----END OPENSSH PRIVATE KEY-----\n"}
  3. {"login":"junisco","password":"gehHeim"}

Note how the last two actually are JSON dicts which contain the elements specified in the key created in the key store.

So I then tried in Python:

#!/usr/bin/env python3

from cryptography.hazmat.primitives.ciphers.aead import AESGCM
from base64 import b64decode
import sys

key = b64decode("Q36I3zFl+CQz8F1mkeT0d8pbsQC0qksfqvWI1vLYDnU=")
blob = b64decode("cIRZIUUaFCxajOak6I51agLyqm7vlL/HL+AQfaIr8yU6VGA7HiVgOR7/aqB+PPZ28Xk=")

nonce = blob[:12]
ciphertext = blob[12:]

try:
    aesgcm = AESGCM(key)
    plaintext = aesgcm.decrypt(nonce=nonce, data=ciphertext, associated_data=None)
    print(plaintext.decode())
except Exception as e:
    print("Decryption failed: {0}".format(str(e)), file=sys.stderr)

which works fine:

$ ./jp.py
Full of coffee, please

This demonstrates that we should keep our Semaphore UI’s config.json safe and back it up securely.

ansible and semaphore-ui :: 28 Oct 2025 :: e-mail

Three years ago, a friend of mine purchased a server (one of those horrendously loud things most of us mere mortals thankfully seldom or never get to hear) from a hosting company. The server held a to-him precious application served by a proprietary content management system. He abandoned the CMS at the time because, as I understand it, the monthly price of the CMS was to increase four-fold.

Stuck in a basement, the server itself gathered more dust for three years until my friend decided he wanted the data. The hosting company had told him “just connect it to the internet and you can access your data” which, obviously, is total bollocks – it begins with not owning the domain and ends with a hard-coded IP which belongs to said hosting company.

He asked me for help. I sighed. I helped.

For the first time in quite some years I really got my hands dirty, so to speak. I went to his house at 17:30 and got home again past midnight. I was, though, given a nice dinner, and the lady of the house had baked my favorite cake!

I’d brought a router/switch which we used to get this screaming beast lying on the table before us networked. I then had to remember what little I knew of Proxmox, and got as far as seeing there was a qemu container with id 100.

The guest tools (if I recall the name correctly) weren’t installed in the container so I couldn’t do much from outside, but I was able to vzdump the container into a file on the host which thankfully had sufficient disk space. After extracting that dump I had the disk image which I could losetup / mount, and a chroot later I was “in”. I asked for and received a small brandy.

I copied the data onto a portable USB disks (my can those things be slow!) and created a MySQL dump of the database. All in all some 20 GB of data were shuffled in and out.

This morning, then finally, I discovered an absolutely gorgeous little tool called mysql2sqlite with which I created a SQLite database file which I zipped up and sent to my friend with the instruction to install DB Browser for SQLite for viewing the database.

It’s been a very long time since I’ve done this type of messing about, but it was fun, if exhausting.

helping :: 31 Aug 2025 :: e-mail

The year is (roughly) 1989, and we have a small office with some Unix computers and a handful of Facit A2400 terminals connected to them. What we love most about these terminals is they are positive terminals with black text on a white background. We “grew up” with green on black and, later at Nixdorf Computer AG, with amber on black.

Young readers might be surprised we used to get printed manuals with our terminals (and computers); don’t forget, the Web didn’t exist then, and “download the manual as PDF” hadn’t yet been invented.

a printed manual and some RS232 connectors

I spent untold and countless hours developing a special curses library (dubbed “Ecurses”) with special input functions etc. for customers whom we also recommended these terminals to.

When we gave up that office, I took two or three of the terminals along, even a brand new one which, stupidly, I dumped at recycling many years later. Only one of the Facits permanently in use survived; the dirty case and the old Duesseldorf zip code on the service sticker are proof.

the original sticker with the old Duesseldorf zip code and telephone prefix

The year is now (exactly) 2025, and Martin S. of the Linuxhotel has the idea of setting up an “old” terminal to show trainees in Unix beginner courses what our life was once like. I tell him the story of the Facits and promise to bring one along to lend to him.

Obviously I’m not going to schlepp this very heavy terminal to Essen just to determine it no longer works, so I configure a Shuttle PC with OpenBSD and set up com0 as the console at 19200 baud; the Facit can do double that, but I thought this would be a compromise between “speed” and “seeing slowish output” for noobs.

% cat /etc/boot.conf
stty com0 19200
set tty com0

% grep tty00 /etc/ttys
tty00	"/usr/libexec/getty std.19200"	vt220	 on secure

The most difficult part of the whole operation was finding the correct cable, obviously. I used to be inundated in cables but got rid of most many years ago. Luckily a trip to the cellar uncovered the needed combination, so I booted up.

an almost new Facit A2400 displaying an OpenBSD 7.7 console

Astute readers will notice there’s no ESCape key on the keyboard, but it can be mapped to the compose key, and for those who want to avoid doing that, ESCape is also, and has always been, CTRL-[ (octal 33, hex 1B, decimal 27).

on-screen setup

I have decided to make this a permanent loan to the lovely people at the Linuxhotel and very much hope younger generations will have the opportunity of experiencing what we used to work with.

further reading

retro, openbsd, unix, and historic :: 26 Aug 2025 :: e-mail

Other recent entries