❌

Normale weergave

Reproducible Builds (diffoscope): diffoscope 332 released

28 September 2026 om 02:00

The diffoscope maintainers are pleased to announce the release of diffoscope version 332. This version includes the following changes:

[ Chris Lamb ]
* Do not Build-Depend on GNU Guix. Thanks to Vagrant and Holger for the
  reports. (Closes: #1149132)

[ Guillaume Girol ]
* Update more tests to support new versions of Radare.
  (Closes: reproducible-builds/diffoscope#432)

You find out more by visiting the project homepage.

  •  

Otto Kekäläinen: Using multiple git remotes for true distributed version control

28 September 2026 om 02:00
Featured image of post Using multiple git remotes for true distributed version control

One of the primary design principles for Linus Torvalds when creating Git is that it should be a distributed system, capable of working with multiple remotes. However, most people seem to use Git with just one remote, typically having GitHub or GitLab as the only origin.

Version control systems before Git relied on a centralized model. For example in Subversion or CVS you could only sync with one single remote server, which was considered the primary host of the source code. Git however is not just a VCS, but a DVCS, for distributed version control system. If you currently use Git with just one single remote as if it was a centralized version control system, now is the time to change that and learn how to add multiple remotes, and configure more flexible git pull and git push rules.

The first concept to grasp is that Git can have multiple remotes. For example, after running a basic clone command such as git clone https://github.com/ottok/debcraft.git you would end up with a single remote origin pointing to GitHub:

$ git remote --verbose origin https://github.com/ottok/debcraft.git (fetch) origin https://github.com/ottok/debcraft.git (push)
$ git remote --verbose
origin https://github.com/ottok/debcraft.git (fetch)
origin https://github.com/ottok/debcraft.git (push)

You can however configure multiple remotes with the command git remote:

$ git remote rename origin github $ git remote add gitlab https://gitlab.com/ottok/debcraft.git $ git remote --verbose github https://github.com/ottok/debcraft.git (fetch) github https://github.com/ottok/debcraft.git (push) gitlab https://gitlab.com/ottok/debcraft.git (fetch) gitlab https://gitlab.com/ottok/debcraft.git (push)
$ git remote rename origin github
$ git remote add gitlab https://gitlab.com/ottok/debcraft.git
$ git remote --verbose
github https://github.com/ottok/debcraft.git (fetch)
github https://github.com/ottok/debcraft.git (push)
gitlab https://gitlab.com/ottok/debcraft.git (fetch)
gitlab https://gitlab.com/ottok/debcraft.git (push)

You can push and pull from these individually, or from all at once:

$ git pull --all Fetching github Fetching gitlab Already up to date.
$ git pull --all
Fetching github
Fetching gitlab
Already up to date.

When you review the git history, note the labels gitlab/main and github/main which tell which commit the branch main points to on various remotes. In this case they are all in sync and point to the same 5d1815c:

$ git log --oneline 5d1815c (HEAD -> main, gitlab/main, github/main) Bump Debhelper version from 13 to 14 ca518a7 Temporarily disable automatic copyright year bumping due to frequent bugs d6e201e Exclude .pm from codespell to decrease false positives (Closes: #1140488) 708ee35 Exclude debian/copyright from codespell due to frequent false positives
$ git log --oneline
5d1815c (HEAD -> main, gitlab/main, github/main) Bump Debhelper version from 13 to 14
ca518a7 Temporarily disable automatic copyright year bumping due to frequent bugs
d6e201e Exclude .pm from codespell to decrease false positives (Closes: #1140488)
708ee35 Exclude debian/copyright from codespell due to frequent false positives

When pushing, you can choose what remote to push to. For example, if I am working on branch dev and I only want to push it to GitLab to trigger a CI run there, I can run git push gitlab dev. You can also configure a default remote by running once git push --set-upstream gitlab dev and thereafter simply run git push and it will automatically push the branch dev only to that remote.

A related setting is remote.pushDefault, which tells git which remote to push to when a branch does not define one of its own, and it applies to every branch that lacks a [branch] section in .git/config. Setting it once is what lets a repository have a per-branch pull remote, like the debcraft example below, and still have a single, predictable git push target. To see which remote git will use for each of your branches, run git branch -vv, and to refresh the remote-tracking branches of all remotes at once, run git fetch --all --prune.

If you want to have multiple remotes, but automatically push and pull to all of them at once, you configure one single remote to have multiple URLs with git remote set-url --add. Since git prints one line per push URL, git remote -v then shows the same remote several times, once for each URL it pushes to.

As an illustration, in my actual local Debcraft development project I have all these configured:

$ git remote -v github-pull-requests git@github.com:ottok/debcraft.git (fetch) github-pull-requests git@github.com:ottok/debcraft.git (push) gitlab-merge-requests git@gitlab.com:ottok/debcraft.git (fetch) gitlab-merge-requests git@gitlab.com:ottok/debcraft.git (push) origin git@salsa.debian.org:debian/debcraft.git (fetch) origin git@salsa.debian.org:debian/debcraft.git (push) origin git@gitlab.com:ottok/debcraft.git (push) origin git@github.com:ottok/debcraft.git (push) otto git@salsa.debian.org:otto/debcraft.git (fetch) otto git@salsa.debian.org:otto/debcraft.git (push) otto git@gitlab.com:ottok/debcraft.git (push) otto git@github.com:ottok/debcraft.git (push) otto git@git.sr.ht:~ottok/debcraft (push) otto ssh://git@codeberg.org/ottok/Debcraft.git (push) salsa-merge-requests git@salsa.debian.org:debian/debcraft.git (fetch) salsa-merge-requests git@salsa.debian.org:debian/debcraft.git (push)
$ git remote -v
github-pull-requests git@github.com:ottok/debcraft.git (fetch)
github-pull-requests git@github.com:ottok/debcraft.git (push)
gitlab-merge-requests git@gitlab.com:ottok/debcraft.git (fetch)
gitlab-merge-requests git@gitlab.com:ottok/debcraft.git (push)
origin git@salsa.debian.org:debian/debcraft.git (fetch)
origin git@salsa.debian.org:debian/debcraft.git (push)
origin git@gitlab.com:ottok/debcraft.git (push)
origin git@github.com:ottok/debcraft.git (push)
otto git@salsa.debian.org:otto/debcraft.git (fetch)
otto git@salsa.debian.org:otto/debcraft.git (push)
otto git@gitlab.com:ottok/debcraft.git (push)
otto git@github.com:ottok/debcraft.git (push)
otto git@git.sr.ht:~ottok/debcraft (push)
otto ssh://git@codeberg.org/ottok/Debcraft.git (push)
salsa-merge-requests git@salsa.debian.org:debian/debcraft.git (fetch)
salsa-merge-requests git@salsa.debian.org:debian/debcraft.git (push)

The settings can also be viewed directly in the config file:

$ cat .git/config ... [remote "origin"] url = git@salsa.debian.org:debian/debcraft.git fetch = +refs/heads/*:refs/remotes/origin/* pushurl = git@salsa.debian.org:debian/debcraft.git pushurl = git@gitlab.com:ottok/debcraft.git pushurl = git@github.com:ottok/debcraft.git [remote "otto"] url = git@salsa.debian.org:otto/debcraft.git fetch = +refs/heads/*:refs/remotes/otto/* pushurl = git@salsa.debian.org:otto/debcraft.git pushurl = git@gitlab.com:ottok/debcraft.git pushurl = git@github.com:ottok/debcraft.git pushurl = git@git.sr.ht:~ottok/debcraft pushurl = ssh://git@codeberg.org/ottok/Debcraft.git ... [branch "main"] remote = origin merge = refs/heads/main [branch "dev"] remote = otto merge = refs/heads/dev ...
$ cat .git/config
...
[remote "origin"]
url = git@salsa.debian.org:debian/debcraft.git
fetch = +refs/heads/*:refs/remotes/origin/*
pushurl = git@salsa.debian.org:debian/debcraft.git
pushurl = git@gitlab.com:ottok/debcraft.git
pushurl = git@github.com:ottok/debcraft.git
[remote "otto"]
url = git@salsa.debian.org:otto/debcraft.git
fetch = +refs/heads/*:refs/remotes/otto/*
pushurl = git@salsa.debian.org:otto/debcraft.git
pushurl = git@gitlab.com:ottok/debcraft.git
pushurl = git@github.com:ottok/debcraft.git
pushurl = git@git.sr.ht:~ottok/debcraft
pushurl = ssh://git@codeberg.org/ottok/Debcraft.git
...
[branch "main"]
remote = origin
merge = refs/heads/main
[branch "dev"]
remote = otto
merge = refs/heads/dev
...

On branch main:

  • Running git pull will fetch from salsa.debian.org/debian/debcraft (the primary git hosting for this project)
  • Running git push will push to Salsa, GitLab and GitHub, one after the other, making sure they all have the latest main

On branch dev:

  • Running git pull will fetch from salsa.debian.org/otto/debcraft (the personal fork)
  • Running git push will push to Salsa, GitLab, GitHub, Sourcehut and Codeberg all one after the other

The command output will have the same message three times, once for each remote (assuming they were all already up-to-date):

$ git push Everything up-to-date Everything up-to-date Everything up-to-date
$ git push
Everything up-to-date
Everything up-to-date
Everything up-to-date

Use cases for distributed version control

The most obvious use case for any distributed system is improved availability. In this example, the salsa.debian.org is often overloaded/unavailable, so pushing to GitLab and GitHub as well allows collaborators to use them to fetch git commits if the primary code hosting site is unresponsive. Once it comes back online again, the next git push/pull will automatically bring it up to the same content as what backup hosts had.

The second main benefit is that having the code live on multiple code forges allows collaborators to use their own favorite host. Debcraft itself is a great example of this, as collaborators who don’t have a Salsa account can - and have - submitted Merge Requests on GitLab.com and Pull Requests on GitHub.com.

A third use case is code hosting migrations. If you for example want to gradually phase out GitHub.com and replace it with something else, you could configure git to pull from the old location and push to both, ensuring that other collaborators can start using the new code hosting location without disruptions to those who have not yet updated their remotes or done a fresh git clone from the new location.

Automating setting up a project on multiple code forges

While I don’t push all projects I work on to all Salsa/GitLab/GitHub, I do set up multiple remotes for enough git projects that I have this script to automate configuring a git repository that is cloned from Salsa to also push to GitLab and GitHub. It accepts the repository whether it was cloned over SSH or HTTPS, and it adds the push URLs and then pushes the current branch, all branches, and tags for you:

#!/bin/bash # Add GitLab and GitHub push URLs to an existing git remote, so that a plain # "git push" writes the same commits to all three code forges. # # Usage: ./git-multipush-salsa.sh # # Environment: # SALSA_REMOTE remote to turn into a mirror, default origin # SALSA_USER your Salsa user, only used to detect that the source # remote is somebody else's project # GITLAB_USER your GitLab user, default ottok # GITHUB_USER your GitHub user, default ottok # PROTOCOL ssh (default) or https, for the push URLs set -euo pipefail SCRIPT=$(basename "$0") SALSA_REMOTE=${SALSA_REMOTE:-origin} SALSA_USER=${SALSA_USER:-otto} GITLAB_USER=${GITLAB_USER:-ottok} GITHUB_USER=${GITHUB_USER:-ottok} PROTOCOL=${PROTOCOL:-ssh} die() { echo "$SCRIPT: $*" >&2; exit 1; } # A failing command leaves the push URLs half configured behind, so say how # to get back to where we started fail() { echo "$SCRIPT: '$*' failed, the push URLs may already have been added" >&2 echo " git config --unset-all remote.${SALSA_REMOTE}.pushurl" >&2 echo " git remote set-url --add --push ${SALSA_REMOTE} ${SALSA_URL}" >&2 exit 1 } git rev-parse --git-dir > /dev/null 2>&1 || die "not inside a git repository" git remote | grep -qx "$SALSA_REMOTE" || die "no remote named '$SALSA_REMOTE'" # Example: git@salsa.debian.org:otto/salsa-ci-pipeline.git SALSA_URL=$(git remote get-url "$SALSA_REMOTE") # The source remote may use any of the URL forms salsa.debian.org accepts, # for example git@salsa.debian.org:otto/salsa-ci-pipeline.git or # https://salsa.debian.org/otto/salsa-ci-pipeline.git case "$SALSA_URL" in git@salsa.debian.org:*) SALSA_PATH=${SALSA_URL#git@salsa.debian.org:} ;; ssh://git@salsa.debian.org/*) SALSA_PATH=${SALSA_URL#ssh://git@salsa.debian.org/} ;; https://salsa.debian.org/*) SALSA_PATH=${SALSA_URL#https://salsa.debian.org/} ;; http://salsa.debian.org/*) SALSA_PATH=${SALSA_URL#http://salsa.debian.org/} ;; *) die "unexpected source URL '$SALSA_URL', expected a salsa.debian.org URL" ;; esac SALSA_PATH=${SALSA_PATH%/} SALSA_PATH=${SALSA_PATH%.git} SOURCE_USER=${SALSA_PATH%%/*} PROJECT=${SALSA_PATH##*/} # Write the push URLs in one and the same protocol, no matter how the # repository happens to have been cloned if [ "$PROTOCOL" = "https" ] then SALSA_URL="https://salsa.debian.org/${SALSA_PATH}.git" GITLAB_URL="https://gitlab.com/${GITLAB_USER}/${PROJECT}.git" GITHUB_URL="https://github.com/${GITHUB_USER}/${PROJECT}.git" else SALSA_URL="git@salsa.debian.org:${SALSA_PATH}.git" GITLAB_URL="git@gitlab.com:${GITLAB_USER}/${PROJECT}.git" GITHUB_URL="git@github.com:${GITHUB_USER}/${PROJECT}.git" fi # Mirroring somebody else's project is fine, but it is usually not what you # want: you most likely want to mirror your own fork instead if [ "$SOURCE_USER" != "$SALSA_USER" ] then echo "Note: $SALSA_REMOTE points to $SOURCE_USER's project, not your own." echo "To mirror your own fork, run this with SALSA_REMOTE set to its name." echo fi # Bail out instead of adding the same push URLs twice EXISTING=$(git remote get-url --push --all "$SALSA_REMOTE") if [ "$(printf '%s\n' "$EXISTING" | wc -l)" -gt 1 ] then die "remote '$SALSA_REMOTE' already has multiple push URLs: $EXISTING" fi echo "Mirroring $SALSA_URL to $GITLAB_URL and $GITHUB_URL" git remote set-url --add --push "$SALSA_REMOTE" "$SALSA_URL" git remote set-url --add --push "$SALSA_REMOTE" "$GITLAB_URL" git remote set-url --add --push "$SALSA_REMOTE" "$GITHUB_URL" # Push the branch you have checked out first, and never with --force: the # first branch a GitLab or GitHub project receives becomes its default branch, # the one you then protect DEFAULT_BRANCH=$(git symbolic-ref --short HEAD) || die "not on a branch, check out a branch first" git push -u "$SALSA_REMOTE" "$DEFAULT_BRANCH" git push "$SALSA_REMOTE" --all git push "$SALSA_REMOTE" --tags # The push above creates the GitLab and GitHub projects, but GitLab projects # are private by default, so make the new mirror public with glab if installed. if command -v glab > /dev/null then echo "Make the GitLab project public and describe it as a mirror:" glab api -X PUT "projects/${GITLAB_USER}%2F${PROJECT}" \ -f visibility=public \ -f description="Mirror of ${SALSA_URL}" \ -f notification_email=disabled else echo "Install glab to do the same automatically, or visit" echo "https://gitlab.com/${GITLAB_USER}/${PROJECT}/edit to make the project public" fi echo "To undo, drop all push URLs and add back only the original one:" echo " git config --unset-all remote.${SALSA_REMOTE}.pushurl" echo " git remote set-url --add --push ${SALSA_REMOTE} ${SALSA_URL}"
#!/bin/bash
# Add GitLab and GitHub push URLs to an existing git remote, so that a plain
# "git push" writes the same commits to all three code forges.
#
# Usage: ./git-multipush-salsa.sh
#
# Environment:
# SALSA_REMOTE remote to turn into a mirror, default origin
# SALSA_USER your Salsa user, only used to detect that the source
# remote is somebody else's project
# GITLAB_USER your GitLab user, default ottok
# GITHUB_USER your GitHub user, default ottok
# PROTOCOL ssh (default) or https, for the push URLs
set -euo pipefail
SCRIPT=$(basename "$0")
SALSA_REMOTE=${SALSA_REMOTE:-origin}
SALSA_USER=${SALSA_USER:-otto}
GITLAB_USER=${GITLAB_USER:-ottok}
GITHUB_USER=${GITHUB_USER:-ottok}
PROTOCOL=${PROTOCOL:-ssh}
die() { echo "$SCRIPT: $*" >&2; exit 1; }
# A failing command leaves the push URLs half configured behind, so say how
# to get back to where we started
fail()
{
echo "$SCRIPT: '$*' failed, the push URLs may already have been added" >&2
echo " git config --unset-all remote.${SALSA_REMOTE}.pushurl" >&2
echo " git remote set-url --add --push ${SALSA_REMOTE} ${SALSA_URL}" >&2
exit 1
}
git rev-parse --git-dir > /dev/null 2>&1 || die "not inside a git repository"
git remote | grep -qx "$SALSA_REMOTE" || die "no remote named '$SALSA_REMOTE'"
# Example: git@salsa.debian.org:otto/salsa-ci-pipeline.git
SALSA_URL=$(git remote get-url "$SALSA_REMOTE")
# The source remote may use any of the URL forms salsa.debian.org accepts,
# for example git@salsa.debian.org:otto/salsa-ci-pipeline.git or
# https://salsa.debian.org/otto/salsa-ci-pipeline.git
case "$SALSA_URL" in
git@salsa.debian.org:*) SALSA_PATH=${SALSA_URL#git@salsa.debian.org:} ;;
ssh://git@salsa.debian.org/*) SALSA_PATH=${SALSA_URL#ssh://git@salsa.debian.org/} ;;
https://salsa.debian.org/*) SALSA_PATH=${SALSA_URL#https://salsa.debian.org/} ;;
http://salsa.debian.org/*) SALSA_PATH=${SALSA_URL#http://salsa.debian.org/} ;;
*) die "unexpected source URL '$SALSA_URL', expected a salsa.debian.org URL" ;;
esac
SALSA_PATH=${SALSA_PATH%/}
SALSA_PATH=${SALSA_PATH%.git}
SOURCE_USER=${SALSA_PATH%%/*}
PROJECT=${SALSA_PATH##*/}
# Write the push URLs in one and the same protocol, no matter how the
# repository happens to have been cloned
if [ "$PROTOCOL" = "https" ]
then
SALSA_URL="https://salsa.debian.org/${SALSA_PATH}.git"
GITLAB_URL="https://gitlab.com/${GITLAB_USER}/${PROJECT}.git"
GITHUB_URL="https://github.com/${GITHUB_USER}/${PROJECT}.git"
else
SALSA_URL="git@salsa.debian.org:${SALSA_PATH}.git"
GITLAB_URL="git@gitlab.com:${GITLAB_USER}/${PROJECT}.git"
GITHUB_URL="git@github.com:${GITHUB_USER}/${PROJECT}.git"
fi
# Mirroring somebody else's project is fine, but it is usually not what you
# want: you most likely want to mirror your own fork instead
if [ "$SOURCE_USER" != "$SALSA_USER" ]
then
echo "Note: $SALSA_REMOTE points to $SOURCE_USER's project, not your own."
echo "To mirror your own fork, run this with SALSA_REMOTE set to its name."
echo
fi
# Bail out instead of adding the same push URLs twice
EXISTING=$(git remote get-url --push --all "$SALSA_REMOTE")
if [ "$(printf '%s\n' "$EXISTING" | wc -l)" -gt 1 ]
then
die "remote '$SALSA_REMOTE' already has multiple push URLs:
$EXISTING"
fi
echo "Mirroring $SALSA_URL to $GITLAB_URL and $GITHUB_URL"
git remote set-url --add --push "$SALSA_REMOTE" "$SALSA_URL"
git remote set-url --add --push "$SALSA_REMOTE" "$GITLAB_URL"
git remote set-url --add --push "$SALSA_REMOTE" "$GITHUB_URL"
# Push the branch you have checked out first, and never with --force: the
# first branch a GitLab or GitHub project receives becomes its default branch,
# the one you then protect
DEFAULT_BRANCH=$(git symbolic-ref --short HEAD) || die "not on a branch, check out a branch first"
git push -u "$SALSA_REMOTE" "$DEFAULT_BRANCH"
git push "$SALSA_REMOTE" --all
git push "$SALSA_REMOTE" --tags
# The push above creates the GitLab and GitHub projects, but GitLab projects
# are private by default, so make the new mirror public with glab if installed.
if command -v glab > /dev/null
then
echo "Make the GitLab project public and describe it as a mirror:"
glab api -X PUT "projects/${GITLAB_USER}%2F${PROJECT}" \
-f visibility=public \
-f description="Mirror of ${SALSA_URL}" \
-f notification_email=disabled
else
echo "Install glab to do the same automatically, or visit"
echo "https://gitlab.com/${GITLAB_USER}/${PROJECT}/edit to make the project public"
fi
echo "To undo, drop all push URLs and add back only the original one:"
echo " git config --unset-all remote.${SALSA_REMOTE}.pushurl"
echo " git remote set-url --add --push ${SALSA_REMOTE} ${SALSA_URL}"

If you prefer not to install glab, the last part simply reduces to visiting the GitLab project settings once and ticking the project public, and it is also worth setting a description like Mirror of https://salsa.debian.org/debian/debcraft and disabling notification email there, as nobody wants a mirror sending out a flood of emails every time you push.

There are a few caveats worth knowing about before mirroring everything everywhere. Every push to a remote with multiple push URLs writes to all of them, so a project with continuous integration configured on three forges will run its CI three times and cost you roughly three times as much. If one of the push URLs fails, say because the host is momentarily unreachable, git prints the error and stops there, leaving the remotes listed after the failing one without the new commits. Simply re-run the push once the host is reachable again; the remotes that did get the commits will just respond with Everything up-to-date. And you should never force-push to only one of the remotes, as that leaves the mirrors permanently out of sync with your local repository.

Avoid pulling too much

Related to multiple remotes, you should also be aware that git allows one to configure which branches to pull. By default a git clone will track all branches from the remote and pull everything, which in a typical open source project will bring in a lot of temporary development branches in vain.

Thus it is better to run git clone with the --single-branch and --branch parameters to fetch only the branch that matters, which in most cases is the main branch. That can be accomplished with:

git clone --single-branch --branch main https://github.com/ottok/debcraft.git
git clone --single-branch --branch main https://github.com/ottok/debcraft.git

If a repository was already cloned, it can be configured to track only one branch with:

git config remote.upstream.fetch "+refs/heads/main:refs/remotes/upstream/main" git fetch upstream git config --get-all remote.upstream.fetch +refs/heads/main:refs/remotes/upstream/main
git config remote.upstream.fetch "+refs/heads/main:refs/remotes/upstream/main"
git fetch upstream
git config --get-all remote.upstream.fetch
+refs/heads/main:refs/remotes/upstream/main

If you later need to track more branches, you can configure them with commands such as:

git config --add remote.upstream.fetch "+refs/heads/3.2.y:refs/remotes/upstream/3.2.y" git fetch upstream git checkout 3.2.y
git config --add remote.upstream.fetch "+refs/heads/3.2.y:refs/remotes/upstream/3.2.y"
git fetch upstream
git checkout 3.2.y

For additional details about any of the tips in this post I recommend reading the git man pages.

Now go and spread your code around

Distributing your code over multiple code forges costs you almost nothing in configuration, and in return you get availability, a choice of host for your collaborators, and freedom to migrate without a big bang. Add the push URLs to the remotes you already have, push once, and you are done.

The rest of the git fundamentals are covered in my other posts. If your commits are not yet polished, start with what makes a good git commit message and git commit messages by example. If you work on Debian packaging, Git-based collaboration in Debian explains why the Salsa merge request workflow is built the way it is, and Salsa merge request best practices covers the review process itself.

  •  

Dima Kogan: Lunar terminator paradox

27 September 2026 om 23:04

There's an optical illusion known by names like the "lunar terminator paradox". This has much discussion and descriptions online, but none made sense in my head, so I wrote this program to figure out what was going on. The paradox:

  • The moon is illuminated by the sun
  • After sunset, the sun is below, so we would expect the illuminated portion of the moon to point down, towards where the sun is
  • This isn't what always happens!

I was camping in the mountains with a friend recently. We were looking up at the sky, just after the sun has set. The moon was clearly pointing up.

After playing with this program, I have clarity: we're looking up at the moon from below. The most pathological case is a full moon. Let's say the moon is on the horizon: elevation=0. If we're looking at this low moon, and the moon is full, the sun is directly behind us. The moon is fully illuminated, and we see 100% of the moon disc lit up.

Now, what happens if the moon is not at the horizon, but higher up at the sky? Effectively, the sun is infinitely far away, so the same part of the moon is lit up. But we're now looking at the moon from below, and we would see some of the dark side on the bottom edge. I.e. the moon appears bright at the top, but has a dark slice missing at the bottom: it points up! Even at sunset.

This is the most clearly weird case. A half-moon has no paradoxical behavior, and the more full the moon, the more clearly we can observe the bright moon pointing up despite the sun being down.

I produce two plots. At 3/4-full moon at sunset we look at the moon as it rises from the horizon all the way up to the zenith:

scan-moon-el.svg

If the moon is at the horizon (elevation = 0), we see the expected 3/4-lit disc. As the moon rises, we see more and more of its bottom, and thus more and more dark areas show up: It clearly points up at sunset!

We can also move the sun around. Let's say the moon is 60deg up; let's move the sun from new-moon to a full-moon:

scan-sun-az.svg

In the darkest configuration, we can still see a lit crescent on the bottom: the illumination is all on the opposite side, but we can see a slice of the illuminated back side. As the sun swings around, we see more and more of the moon. At azimuth = 90deg, we have a half-lit moon. Past that, more and more of the moon is visible, always pointing up.

This was also a super interesting study of the current state of AI tools. As I was writing this and debugging, I would talk to Claude and Gemini about how this worked and what I should be seeing. They were both completely unable to comprehend this, and would make infinite circular arguments. Clearly they read some incomplete explanations on the internet (as I have), and lack the reasoning capability necessary to distill it into something resembling "understanding". AGI is not here yet.

  •  

Dirk Eddelbuettel: #060: Using bubblewrap for R sandboxing

27 September 2026 om 18:02

Welcome to post 60 in the R4 series.

bubblewrap is a great tool and very suitable for using with an agent harness. It is a very compelling—and lightweight—alternative to using a full-blown docker container as it offers low-level unprivileged sandboxing on Linux hosts.

In a nutshell, bubblewrap can ‘turn everything off’ (see unshare-all below) and allow access only to selected services and directories (as shown below). This makes it a very useful tool be used on a main workstation as it can provide a lower-risk deployment quite easily while taking advantage of the already installed software stack. I continue to get a lot of value out of docker, especially as r2u makes installing R package dependencies so trivial. Its slogan ‘easy, fast, reliable: pick all three’ clearly holds for r2u. But sometimes bubblewrap is compelling, for example to launch opencode (documented for example at the debian-inference site).

When using R we need add more directories to the example to provide /etc/alternatives which is governing inter alia the LAPACK / BLAS resolution on Debian / Ubuntu systems; this is now on debian-inference site but wasn’t when I first tried it a few days ago ;-) as well as /etc/R for config files and possibly ~/.R/Makevars for compiler settings. With that my current wrapper is

#!/bin/bash
#
# cf https://inference.debian.net/doc
#    and 'curl' command to set bearer code
bwrap \
  --ro-bind /usr /usr \
  --symlink usr/bin /bin \
  --symlink usr/lib /lib \
  --symlink usr/lib64 /lib64 \
  --ro-bind /etc/ssl /etc/ssl \
  --ro-bind /etc/resolv.conf /etc/resolv.conf \
  --ro-bind $HOME/.gitconfig $HOME/.gitconfig \
  --bind $HOME/.config/opencode $HOME/.config/opencode \
  --bind $HOME/.cache/opencode $HOME/.cache/opencode \
  --bind $HOME/.opencode $HOME/.opencode \
  --bind $HOME/.local/share/opencode $HOME/.local/share/opencode \
  --bind $HOME/.local/state/opencode $HOME/.local/state/opencode \
  --ro-bind $HOME/.R/Makevars $HOME/.R/Makevars \
  --ro-bind /etc/R/ /etc/R \
  --ro-bind /etc/alternatives/ /etc/alternatives \
  --proc /proc \
  --dev /dev \
  --tmpfs /tmp \
  --bind $(pwd) $(pwd) \
  --chdir $(pwd) \
  --unshare-all \
  --share-net \
  --die-with-parent \
  /usr/local/bin/opencode "$@"

Launched that way we can have agents use R to check packages sources, builds, and triage for bugs. this works equally well with local models served via ollama (and with that a quick shoutout to packages.lingfish.net for providing an apt service for ollama) as it does with the various cloud-based offerings. So harness away with R!

This post by Dirk Eddelbuettel originated on his Thinking inside the box blog. If you like this or other open-source work I do, you can now sponsor me at GitHub.

  •  

Aquila Macedo Costa: Testing library transitions before they reach unstable

27 September 2026 om 07:58

A library transition can affect many packages in Debian. I added a way for Salsa CI to rebuild the ones that depend on the changing library, so maintainers can test the transition before it reaches unstable.

The need for this became clear during a proposed Poppler transition. Poppler is a library for rendering PDFs. The reverse-dependency job I had added to Salsa CI queued 100 of 115 candidate rebuilds, even though only about 21 were relevant. The problem was documented in issue #571.

Before this change, ratt used every binary package produced by Poppler to decide what to rebuild. That also picked up packages unrelated to the library transition. The tracker listed the old and new library package names, so I used them to focus the selection.

Poppler transition tracker showing its Affected, Good, and Bad expressions The Poppler transition tracker identifies the old and new runtime library package names.

I added -transition_affected to ratt to implement this selection. The option performs the scan against the selected archive indexes and injects locally built .deb files into each source rebuild. It expects a regex of dependency package names, not a complete Ben expression, so the tracker’s Affected expression must be adapted first.

I then integrated the mode into Salsa CI. Maintainers can enable transition-aware rebuilds by setting SALSA_CI_BUILD_REVERSE_DEPENDENCIES_TRANSITION_AFFECTED_REGEX. If the variable is unset, the pipeline keeps the existing selection.

Salsa CI pipeline showing selected Poppler rebuild jobs A Poppler test showing the selected rebuild jobs. Three jobs reached the CI timeout, they were not confirmed package regressions.

I also changed ratt to cover packages targeting experimental. By default, it resolves and rebuilds reverse dependencies from unstable while still injecting the locally built .deb files. This lets a maintainer test a transition staged in experimental against packages currently in unstable.

Together, these changes help maintainers focus CI on packages relevant to a library transition and review rebuild results before the new library reaches unstable.

  •  

Russ Allbery: Review: Ode to the Half-Broken

27 September 2026 om 04:57

Review: Ode to the Half-Broken, by Suzanne Palmer

Publisher: DAW
Copyright: May 2026
ISBN: 0-7564-2013-X
Format: Kindle
Pages: 441

Ode to the Half-Broken is a standalone post-apocalyptic (sort of) science fiction novel.

As this novel opens, the unnamed first-person protagonist is waking up in a bathtub in an abandoned building. They are covered in electromagnets and missing one leg.

Waking up involved a secure reboot from a protected core kernel, so it is immediately obvious that the protagonist is a robot. That also explains the electromagnets, which interfere with their ability to control their body. Or they would, if the protagonist didn't have long-dormant combat routines available to deploy EM shielding and free themselves from the bathtub. There are no immediate answers to why someone embedded a virus in the data attached to a scientific paper on ants, an act so precisely targeted that it had to be personal. Or what happened to their leg.

"There are a number of electromagnets in the next room," I say. I point. "Behind that wall with all the holes in it."

"Those are big holes. And fresh, too," the dog says. "I wonder what made them."

"Electromagnets experiencing sudden velocity."

The dog gives another of its short barks, and I am more convinced now it is laughing, though I was not attempting to be funny.

Ode to the Half-Broken has a classic science fiction beginning: in medias res in a future science fiction version of Earth where nothing is immediately explained, leaving the reader to work out the date and the setting. Helping us out is the protagonist, who is neither chatty nor very sociable with other characters but keeps up an analytical monologue about the world as they narrate events.

We are some decades into a grim future. The United States has largely collapsed. Large parts of New York City are dangerous to humans due to disease and radiation. Neither of those hazards bother the large number of sentient robots; their scarcity, and therefore economy, is instead based on power. Robots that control solar power stations act as low-level mob bosses, trading power for information or labor.

Our protagonist doesn't want to care about any of this. They retreated from the world long ago for a solitary life in the former New York Botanical Gardens studying ants. But they need a new leg and the best robot repair person nearby is a human woman outside of the city, so they reluctantly head in that direction, accompanied somewhat surprisingly by a cyborg dog.

The typical science fiction apocalypse is caused by something specific. Classically, that event is a nuclear war, but writers found various alternatives before, during, and after the Cold War era: alien invasions, meteor showers, plagues, sea level rise, and peak oil and general environmental collapse, among many others. Less common is a post-apocalyptic novel where the collapse comes from multiple overlapping crises and accumulating failures, from a general failure to meet what Adam Tooze and others call polycrisis. The first time I encountered that style of post-apocalyptic story was in Octavia Butler's Parable of the Sower. It was one of the choices that made (and still makes) that book feel so chillingly realistic.

The world background in Ode to the Half-Broken is shaped by that sort of polycrisis collapse. There is no one reason why New York City is a dangerous, irradiated ruin and society has collapsed into isolated and sometimes warring factions. It is not runaway artificial intelligence, or at least not directly; sentient robots were a late-stage innovation of the collapsing US civilization and a tool and accelerator of its many wars, but they were not the cause and may be a critical ingredient in building some functional replacement. Instead, society fell apart because we failed to address any of the growing problems and then turned on each other in the resulting ruins. The reader gets an idiosyncratic view of that collapse in flashback chapters intermixed with the main narrative of the initially unnamed robot protagonist, flashbacks told from the perspective of the man who invented the key component for robot sentience. As you would expect, those two narratives are linked. The reader will not discover how they are linked until late in the story.

This book could have been extremely depressing. It is not, which says a great deal about Palmer's skill in telling it. The clinical distance of the protagonist in the early chapters helps considerably: They have rejected involvement in this world and therefore can describe it in detached, factual terms, aided by the tendency of most robots in this story to be slightly more precise, logical, and verbose than the human characters. But as the protagonist's clinical distance fades, as it must to allow the story's emotional stakes to rise, Palmer fills this book with moments of beauty and hope. A former nuclear submarine that has become the mind of a subway car is a key early ally. A collective has turned the Hudson Bridge into a thriving if chaotic community. Self-regulating enclaves of humans and robots are working together to try to build a livable world. Robots oversee pollinator swarms that are attempting to restore plant life, for no reason other than that they like order and stability and see this as a way of restoring both. This is a grim, post-apocalyptic world that does not dwell on either the grimness or the apocalypse. It is instead full of small moments of hope, collaboration, and personal connection.

This is a story with villains, danger, pain, and risk. Despite the similar keywords and cover aesthetics, it bears little resemblance to Becky Chambers's philosophical Monk and Robot series. It is not, in any definition of the term, cozy; it's a post-apocalyptic adventure story about a robot and a dog making their way through a ruined civilization and fighting people who are trying to build something even worse on the rubble. But it's also charming and oddly happy. There are strong found-family vibes in the way that a party coalesces around the protagonist and eventually gives them a name, there is the quiet happiness of seeing a grumpy misanthrope slowly come out of their protective shell, and the rail mechs are an absolute delight.

Like a lot of stories of happy small-group anarchism, I didn't entirely believe the politics and have some qualms about the realism of the ending, although perhaps I should strive to be as optimistic as Palmer writes. The somewhat dry and fussily precise first-person narration will also not be to everyone's taste; it worked for me by the end of the book, but it took a bit of reading to get used to it. But those quibbles aside, this was quietly lovely. It's a satisfying science fiction adventure story in a world with disturbing parallels to the one we're living in, but which uses those parallels to focus on small-scale collective efforts to build something better. And all the small details of the world-building are delightful: the different tiers of robots and their odd behaviors, the machine sense of purpose, even the mix of suspicion and generosity. The plot has large-scale implications, but it was the human-level details that charmed me.

I still want another Fergus novel, but this was a highly successful foray outside of that series. Palmer has won two Hugos for her novellas, so I can't really say she's overlooked, but her novels aren't getting anywhere near the attention they deserve. Highly recommended.

There is plenty of world-building room here for a sequel, but Ode to the Half-Broken reaches a satisfying standalone conclusion. So far as I know, no sequel is currently planned.

Rating: 8 out of 10

  •  

Reproducible Builds (diffoscope): diffoscope 331 released

25 September 2026 om 02:00

The diffoscope maintainers are pleased to announce the release of diffoscope version 331. This version includes the following changes:

[ Chris Lamb ]
* Support radare2 >= 5.9.0. (Closes: reproducible-builds/diffoscope#432)
* Update debian/tests/control.
* Update copyright years.

[ Christopher Baines ]
* Add support for .nar files via Guix.

You find out more by visiting the project homepage.

  •  

Dirk Eddelbuettel: RcppFastAD 0.0.5 on CRAN: Maintenance

24 September 2026 om 21:13

A new release 0.0.5 of the RcppFastAD package is now on CRAN, has been built for r2u, and updated at r-universe. This comes (to the day) two years after the preceding 0.0.4 release.

RcppFastAD wraps the FastAD header-only C++ library by James which provides a C++ implementation of both forward and reverse mode of automatic differentiation. It offers an easy-to-use header library that is both lightweight and performant. With a little of bit of Rcpp glue, it is also easy to use from R in simple C++ applications. This release updates the continuous integration setup as one does, adds a local configuration helper to quieten compilation (described also in this blog post). It also adds a defensive setting for g++: Under recent g++ versions and optimisation at least the -O3 level, segfaults are seen as something is not quite right with (temporary) “views” of Eigen objects in code generated by this versions. Others are fine, as is clang++. We have not gotten to the bottom of it, but setting -g0 seems to ensure that builds generally work.

The NEWS file for this release follows.

Changes in version 0.0.5 (2026-09-24)

  • Several routine updates to continuous integration have been made

  • Local builds (where a .git/ directory is seen) now append silencing compiler option that CRAN would object to

  • Given recent issues with g++ under optimization, debugging is turned off by default.

Courtesy of my CRANberries, there is also a diffstat report for the most recent release. More information is available at the repository or the package page.

This post by Dirk Eddelbuettel originated on his Thinking inside the box blog. If you like this or other open-source work I do, you can now sponsor me at GitHub.

  •  

Russell Coker: Links September 2026

24 September 2026 om 12:33

Bjorn Stahl of the Arcan project wrote an insightful blog post The Day of a new Command-Line Interface: Shell [1]. He does a really good job of identifying problems and strategies for dealing with them, the UI of the results isn’t suitable to what I do though.

Using sqlite as an executable format (replacement for ELF) is one of the wildest ideas I’ve seen in a while, it might actually have some good use cases too [2].

Andrew Dana Hudson wrote an interesting LongNow article about SciFi’s focus on space travel when it seems unlikely [3].

TheDriven has an informative article about the “Hands Off Our Fuel” astroturf campaign that uses farmers as the face of an attempt to give tax exemptions to mining companies [4].

The Conversation has an interesting article about the new Chinese regulations restricting access to AI systems to prevent emotional dependence and prevent minors from having virtual partners or relatives [5].

Elana Hashman wrote an informative blog post about Python virtualenvs, has useful information for people who aren’t Python experts and just need to get stuff done [6].

We need more systems in the NTP pool, I would consider doing this if I had a server with an IP address that won’t change and no risk of bandwidth throttling [7].

Cory Doctorow has some interesting thoughts on the CEO fetish for “AI” even though it is almost never doing any good [8].

Steve Rose wrote an insightful article for The Guardian about the tech fascists of Silicon Valley [9].

Jacqui Lambie calls Pauline Hanson an “angry little woman” and a “bloody coward”, One Nation hate the rule of law [10].

The Conversation has an interesting article about Regime Change, the book about Trump by Maggie Haberman and Jonathan Swan [11].

Elvira Barry wrote an insightful article about the many failures of the Russian legal system, it’s interesting to note that ALL those failures match things that the Trump regime is doing [12].

Pyotr Kurzin – Geopolitics YouTube channel has an interesting interview about “continental” vs “maritime” powers in the context of what the US and Russia are doing [13].

Maxinomica has an interesting video about how China obtained a monopoly on rare earth metals and what happens next [14].

Anarcat wrote an insightful blog post The people vs the AI overlords about the problems that “AI” software development is bringing [15].

John Goerzen wrote an interesting blog post discussing the issues related to LLM use in FOSS projects [16].

Cybernews has an article about an exploit for Samsung, Xiaomi, Oppo, and OnePlus phones due to bad OEM kernel drivers and bad OEM API proxies, no company should release devices with proprietry drivers [17].

Scott Santens wrote an insightful article about how retraining fails laid-off workers and why UBI is needed when workers get replaced by LLMs [18].

libroot has some information on the Snowden archive and why so little of it has been used by journalists, we need more answers about this [19].

Drew Devault’s list of Weird little guys of FOSS is interesting history about some people who are best avoided [20].

Related posts:

  1. Links March 2025 Anarcat’s review of Fish is interesting and shows some benefits...
  2. Links February 2026 Charles Stross has a good theory of why “AI” is...
  3. Links September 2024 CNA Insider has an insightful documentary series about Chinese illegal...
  •  

Mike Gabriel: Looking for Golang + Fullstack Developer with interest in Civic Tech

23 September 2026 om 22:16

Who?

Fre(i)e Software GmbH is looking for a senior Golang + fullstack developer who can handwrite code and act as meticulous code review partner for another senior developer in the company.

What?

Most of your work would result in Open Source contributions to the Voxit project [1], an online civic tech tool for digital participation.

How and Where?

The work can either be delivered on project agreement base via freelancing or via employment (option only available to developers resident in Germany, Austria or Poland).

What next?

If you are interested, please get in touch and provide your hourly rate to us and your average hours of availability per week.

[1] https://gitlab.com/voxit/

  •  

Freexian Collaborators: Monthly report about Debian Long Term Support, August 2026 (by Thorsten Alteholz)

20 September 2026 om 02:00

The Debian LTS Team, funded by Freexian’s Debian LTS offering, is pleased to report its activities for August.

Activity summary

During the month of August, 19 contributors have been paid to work on Debian LTS (links to individual contributor reports are located below).

The team released 58 DLAs fixing 1885 CVEs.

Debian 11 (“bullseye”), which has reached the end of its Long Term Support on 31 August 2026, will now get security support from Freexian under the Extended LTS offer.

The team published several notable updates:

  • chromium (DLA 4710-1), uploaded by Emilio, to fix 375 security flaws in bookworm, including issues that could lead to arbitrary code execution. Further DLA 4728-1, uploaded by Emilio as well, fixed additional 41 security issues in bookworm and DLA 4739-1 fixed 5 issues. Last but not least, DLA 4749-1 fixed another 15 issues and DLA 4758-1 fixed another 7 issues.
  • ruby2.7 (DLA 4716-1), uploaded by Abhijith, to fix four security flaws in bookworm, including issues that could lead to arbitrary code execution.
  • several DLAs (DLA 4717-1, DLA 4720-1, DLA 4723-1, DLA 4724-1, DLA 4745-1) have been uploaded by Ben and Emilio to fix myriad security flaws in different versions of the linux kernel (5.10.262-1, 6.1.180-1, 6.12.100-1~deb12u1, 6.12.101-1~deb12u1) in bullseye and bookworm.
  • 7zip (DLA 4718-1) and p7zip (DLA 4719-1), uploaded by Sylvain, to fix multiple security flaws in bookworm and bullseye, including issues that could yield to remote code execution.
  • ca-certificates (DLA 4726-1), uploaded by Bastien to add some new trusted certificates to bookworm and bullseye, but also remove some untrusted ones.
  • thunderbird (DLA 4727-1 and DLA 4754-1), uploaded by Emilio to fix 35 and 31 issues in bookworm and bullseye.
  • nss (DLA 4729-1), uploaded by Jochen to fix a vulnerability in bookworm and bullseye that could yield to arbitrary code execution.
  • libgd2 (DLA 4731-1), uploaded by Guilhem to fix a vulnerability in bookworm and bullseye that could yield to arbitrary code execution if a malformed GIF file is processed.
  • xorg-server (DLA 4737-1 and DLA 4738-1), uploaded by Arnaud to fix several vulnerabilities in bookworm and bullseye.
  • postgresql-15 (DLA 4740-1), uploaded by Carlos Henrique Lima Melara to fix 26 vulnerabilities in bookworm.
  • unzip (DLA 4741-1), uploaded by Andrej to fix a vulnerability in bookworm and bullseye that could yield to arbitrary code execution.
  • firefox-esr (DLA 4750-1), uploaded by Emilio to fix 31 vulnerabilities in bookworm and bullseye that could potentially result in arbitrary code execution, privilege escalation or information disclosure.
  • nvidia-graphics-drivers (DLA 4752-1 and DLA 4753-1), uploaded by Tobias to fix lots of vulnerabilities in bookworm and bullseye.
  • roundcube (DLA 4760-1), uploaded by Guilhem to fix ten vulnerabilities in bookworm and bullseye that could potentially result in remote code execution or information disclosure.

Contributions from outside the LTS Team:

We are greatly thankful for the contributions from people outside the LTS Team:

  • Salvatore Bonaccorso released a libyaml-syck-perl security update (DLA 4730-1), to fix a denial of service and potentially arbitrary code execution.
  • Thomas Goirand released a neutron security update (DLA 4735-1).
  • Thomas Goirand released an ironic security update (DLA 4743-1).
  • Thomas Goirand released a designate security update (DLA 4751-1), to fix a denial of service or DNS hijacking.

The LTS Team has also contributed with updates to the latest Debian releases:

  • xrdp (DSA 6469-1), prepared by Abhijith to address privilege escalation and arbitrary code execution related flaws.

Other contributions:

Besides the work on security updates, different documentation and tooling changes were needed. This work was mainly done by Sylvain.

Individual Debian LTS contributor reports

Thanks to our sponsors

Sponsors that joined recently are in bold.

  •  

Vincent Bernat: Bot-free self-hosted analytics with GoatCounter on NixOS

20 September 2026 om 21:44

In 2016, I removed Google Analytics from this blog to avoid being complicit in feeding the biggest machine for harvesting personal data. Instead, I relied on GoAccess to analyze my server logs.1 For the past couple of years, the statistics have made no sense, despite my attempts to filter bots: AI scrapers inflate the number of visitors to around 2,000 per day. Eventually, I settled on GoatCounter, an open-source, privacy-friendly web analytics platform. I replaced the JavaScript client to filter bots more aggressively and added a CSS fallback. To improve reliability, I implemented a local proxy running on each of the five web servers serving this blog. The rest of this post details how these pieces fit together and how I deploy them on NixOS. ❄️

Why GoatCounter?

GoatCounter does not collect personal data: instead of storing the reader’s IP address or relying on cookies, it creates a session identifier valid for 8 hours from the user agent and the IP address. Its feature set is modest but sufficient for a blog. If you want to look at the interface, GoatCounter’s author runs a public instance for his site. A hosted version lets you try it before running your own instance. With a single binary and an SQLite database, GoatCounter is one of the lightest self-hosted solutions. Privacy-friendly alternatives, in increasing order of complexity, include Umami, Plausible, and Rybbit.

GoatCounter dashboard showing some statistics from my blog, including an article on the spanning tree protocol with 1,224 views for the past week, the referrers, and the breakdown of browsers (52% Chrome, 32% Firefox, with 16% for Firefox 155 and 6% for Firefox 156)

Custom JavaScript client

GoatCounter includes a small JavaScript client—2,189 bytes minified and gzipped. It ships some features I don’t use: a visitor counter, tracking clicks, configurable settings, etc. I replace it with this function to register a hit:

const count = ({ event, title } = {}) => {
  const params = new URLSearchParams({
    p: event || location.pathname,
    t: title || document.title,
    r: document.referrer,
    q: location.search,
    s: document.documentElement.clientWidth,
    e: !!event,
    rnd: Math.random().toString(36).slice(2, 7),
  });
  fetch(`/count?${params}`, { keepalive: true }).catch(() => {});
};

To filter bots,2 I go the extra mile by requiring a user interaction—an idea I stole from Bear Blog.

let sendHit = () => (sendHit = () => {}, count());
["touchmove", "mousemove", "keydown", "pointerdown"].forEach((eventName) =>
  document.addEventListener(eventName, sendHit, {
    once: true,
    passive: true,
  }),
);

If a reader has disabled JavaScript in their browser, I record the hit using a CSS image. The :hover pseudo-class loads it only after an interaction, another trick stolen from Bear Blog. About 2% of my visitors fit into this bucket.3

<!DOCTYPE html>
<html lang="en" class="nojs">
  <head>
    <script>
      // The JavaScript code for this blog requires ES6
      if ("noModule" in HTMLScriptElement.prototype)
        document.documentElement.classList.remove("nojs");
    </script>
  </head>
  <body>
  <!-- ... -->
    <style>
      .nojs body:hover {
        border-width: 0;
        border-image: url('/count?p=/en/blog/2026-kpi-goodhart&t=Building...&r=NoJS&e=false');
      }
    </style>
  </body>
</html>

Where GoAccess reported around 2,000 visitors a day, GoatCounter counts fewer than 200 humans.4 I assume AI scrapers use a low-effort approach: if the content is available without barriers, as on this blog, they don’t spawn a complex mechanized browser that could trigger a page view. Even crawlers running JavaScript, like Googlebot with its headless Chromium, do not interact with the page and never trigger the events I listen to. The interaction-based “proof of humanity” I use is likely to keep working.5

Local proxy

Five servers across the world in Europe and in North America serve the content of this website, but GoatCounter runs on only one of them. To avoid losing track of visitors when GoatCounter is down, I run a local proxy listening on the same /count endpoint. On each server, it stores the hits in memory with a buffer large enough to survive several days of downtime. It sends them in batches to the upstream backend using the /api/v0/count authenticated endpoint.

🖼 Servers on a map. web02 is in Paris, web03 in Helsinki, web04 in Nuremberg, web05 in Ashburn, web06 in Chicago.

I proposed the code for the proxy in pull request #909. GoatCounter’s maintainer declined to maintain so much code for such a niche use case. As a fellow open-source developer, I often hold the same position for my own projects: a one-time contributor effort may translate into a long-term maintainer commitment.

I expose the endpoint for the proxy on the domain of this website to evade ad blockers. This sounds like I don’t respect the reader’s choice, but as GoatCounter is privacy-friendly, I find it acceptable.

location = /count {
  access_log off;
  proxy_pass http://127.0.0.3:8087/count;
  proxy_pass_request_headers off;
  proxy_set_header Accept-Language $http_accept_language;
  proxy_set_header User-Agent $http_user_agent;
  proxy_set_header X-Real-Ip $remote_addr;
}

Deploying on NixOS

My web servers run NixOS, a declarative Linux distribution with built-in configuration management. I manage this small fleet with Colmena, a stateless deployment tool for NixOS. My configuration is available on GitHub.

Deploying applications in containers

For better isolation, each application runs inside an ephemeral lightweight container, powered by systemd-nspawn. Each container runs a stripped-down NixOS instance. A module wraps NixOS’s containers options to avoid repeating the same options for each application.6 The containers share their network namespace with the host: the additional isolation is not worth the increased complexity. For a smaller footprint, I also disable a few non-essential services.

{ config, lib, ... }:
let
  cfg = config.luffy.containers;
in
{
  # User-configurable settings for our custom module
  options.luffy.containers = lib.mkOption {
    default = { };
    description = "Ephemeral containers sharing the host network.";
    type = lib.types.attrsOf (lib.types.submodule {
      options = {
        config = lib.mkOption {
          type = lib.types.deferredModule;
          default = { };
          description = "NixOS configuration of the container.";
        };
      };
    });
  };

  # Translate our options to NixOS containers
  config = {
    containers = lib.mapAttrs
      (name: container: {
        ephemeral = true;
        autoStart = true;
        privateNetwork = false;
        extraFlags = [ "--resolv-conf=replace-host" ];
        config = {
          imports = [ container.config ];
          networking.firewall.enable = false;
          system.stateVersion = config.system.stateVersion;
          systemd.services = {
            console-getty.enable = false;
            systemd-logind.enable = false;
            systemd-oomd.enable = false;
          };
        };
      })
      cfg;
  };
}

To configure a GoatCounter instance running in a container and listening on 127.0.0.4:8088, we import the module7 and declare the container in the config.luffy.containers attribute set:

{ pkgs, config, ... }: {
  imports = [ ./modules/container.nix ];
  config.luffy.containers.goatcounter = {
    config = {
      services.goatcounter = {
        enable = true;
        address = "127.0.0.4";
        port = 8088;
        proxy = true;
      };
    };
  };
}

As the containers are ephemeral, we need to keep persistent data in directories on the host. We add a mounts option and ask NixOS’s containers to expose the configured directories through the bindMounts option.

{ config, lib, ... }:
let
  cfg = config.luffy.containers;
in
{
  options.luffy.containers = lib.mkOption {
    type = lib.types.attrsOf (lib.types.submodule {
      options = {
        mounts = lib.mkOption {
          type = lib.types.listOf lib.types.str;
          default = [ ];
          description = "Host directories mounted read-write at the same place.";
        };
      };
    });
  };

  config = {
    containers = lib.mapAttrs
      (name: container: {
        bindMounts =
          lib.genAttrs container.mounts (path: { hostPath = path; isReadOnly = false; });
      })
      cfg;
  };
}

For example, to persist GoatCounter’s database in the /var/db/goatcounter directory on the host, we add the directory to the mounts option and alter the service definition to tell GoatCounter where the database is.

{ config, ... }:
let
  databaseDirectory = "/var/db/goatcounter";
in {
  config.luffy.containers.goatcounter = {
    mounts = [ databaseDirectory ];
    config = {
      services.goatcounter = {
        extraArgs = [ "-db=sqlite+${databaseDirectory}/db.sqlite" ];
      };
    };
  };
}

A container may also need some secrets. Colmena can upload secrets without storing them in the Nix store. We add a keys option to our containers. It takes an attribute set mapping secret names to the commands to populate them. Then, the module declares the required secrets to Colmena in the deployment.keys option, makes the container depend on the presence of the secrets, and exposes them to the container.

{ config, lib, ... }:
let
  cfg = config.luffy.containers;
in
{
  options.luffy.containers = lib.mkOption {
    type = lib.types.attrsOf (lib.types.submodule {
      options = {
        keys = lib.mkOption {
          type = lib.types.attrsOf (lib.types.listOf lib.types.str);
          default = { };
          description = "Secrets, as a command to run locally. They are mounted in /etc.";
        };
      };
    });
  };

  config = {
    # Colmena uploads each secret in `/var/keys` and make them available
    # to the group "keys".
    deployment.keys = lib.concatMapAttrs
      (_: container: lib.mapAttrs
        (_: keyCommand: {
          inherit keyCommand;
          group = "keys";
          permissions = "0640";
          destDir = "/var/keys";
        })
        container.keys)
      cfg;

    # The container can only start if the required secrets are available.
    systemd.services = lib.mapAttrs'
      (name: container:
        let
          units = map (key: "${key}-key.service") (lib.attrNames container.keys);
        in
        lib.nameValuePair "container@${name}" {
          requires = units;
          after = units;
        })
      cfg;

    # Mount each secret inside the container.
    containers = lib.mapAttrs
      (name: container: {
        bindMounts = lib.mapAttrs'
          (key: _: lib.nameValuePair "/etc/${key}" {
            hostPath = "/var/keys/${key}";
            isReadOnly = true;
          })
          container.keys;
      })
      cfg;
  };
}

For example, GoatCounter needs credentials to download the GeoIP database. I provide a local command to fetch the secret from my password manager and expose it inside the container through the /etc/goatcounter.env environment file.

{ pkgs, config, ... }: 
let
  keyCommand = variable: [
    "${pkgs.runtimeShell}"
    "-c"
    "pass show personal/nixops/secrets | grep '^${variable}='"
  ];
in {
  config.luffy.containers.goatcounter = {
    keys."goatcounter.env" = keyCommand "GOATCOUNTER_GEODB";
    config = {
      systemd.services.goatcounter.serviceConfig = {
        EnvironmentFile = "/etc/goatcounter.env";
        SupplementaryGroups = [ "keys" ];
      };
    };
  };
}

GoatCounter server

Nixpkgs already packages GoatCounter. By overriding the src and vendorHash attributes, I reuse its definition for my custom version with the proxy:

{ goatcounter, fetchFromGitHub }:
goatcounter.overrideAttrs (_: {
  src = fetchFromGitHub {
    owner = "vincentbernat";
    repo = "goatcounter";
    rev = "feature/proxy";
    hash = "sha256-dJRlQlFu3tjcEgabT1LEbyFrasJlhmYu4L/T7EkoNcY=";
  };
  vendorHash = "sha256-c9Q5OrbZR+q6pD3SgPPWe8JUzcZco1AVUKGaV61k5DE=";
})

I wrote a NixOS module to encapsulate GoatCounter: the container definition, the service definition, and the secrets. The module accepts the following options: package, serve.enable, serve.listenAddress, serve.port, and serve.databaseFile. I already detailed the container configuration in the previous section. In the end, I chose not to reuse the GoatCounter module from NixOS: it’s small, so it’s better to insulate my module from unexpected future changes.

{ config, pkgs, lib, ... }:
let
  cfg = config.luffy.goatcounter;
  databaseDirectory = builtins.dirOf cfg.serve.databaseFile;
  chown = "${pkgs.coreutils}/bin/chown -R";
in {
  config.luffy.containers.goatcounter = {
    config.systemd.services.goatcounter = {
      description = "GoatCounter Web Analytics";
      wantedBy = [ "multi-user.target" ];
      serviceConfig = {
        EnvironmentFile = "/etc/goatcounter.env";
        SupplementaryGroups = [ "keys" ];
        DynamicUser = true;
        Restart = "always";
        ExecStart = lib.escapeShellArgs [
          (lib.getExe cfg.package)
          "serve"
          "-listen=${cfg.serve.listenAddress}:${toString cfg.serve.port}"
          "-tls=none"
          "-db=sqlite+${cfg.serve.databaseFile}"
          "-automigrate"
        ];
        # Transfer database ownership to dynamically assigned user "goatcounter".
        ExecStartPre = "+${chown} goatcounter:goatcounter ${databaseDirectory}";
        ReadWritePaths = databaseDirectory;
      };
    };
  };
}

The following snippet configures GoatCounter to listen on 127.0.0.4:8088:

{
  luffy.goatcounter = {
    serve = {
      enable = true;
      listenAddress = "127.0.0.4";
      port = 8088;
    };
  };
}

The last step is to configure nginx to expose GoatCounter on the Internet. I disable the /count endpoint as the local proxy handles it.

{ config, ... }:
let
  cfg = config.luffy.goatcounter.serve;
in
{
  services.nginx.virtualHosts."goatcounter.luffy.cx" = {
    forceSSL = true;
    locations = {
      "/" = {
        proxyPass = "http://${cfg.listenAddress}:${toString cfg.port}";
      };
      "= /count".extraConfig = ''
        return 404;
      '';
    };
  };
}

GoatCounter proxy

The same NixOS module configures the local proxy, with the following options: proxy.enable, proxy.listenAddress, proxy.port, and proxy.site—the site receiving the batches of page views. The local proxy has no persistent data, but it needs the API key to authenticate to the main GoatCounter instance: its container uses the keys option but not the mounts option.

{ config, pkgs, lib, ... }:
let
  cfg = config.luffy.goatcounter;
  keyCommand = _: [ "…" ];
in
{
  config.luffy.containers.goatcounter-proxy = {
    keys."goatcounter-proxy.env" = keyCommand "GOATCOUNTER_API_KEY";
    config.systemd.services.goatcounter = {
      description = "GoatCounter Proxy.";
      wantedBy = [ "multi-user.target" ];
      serviceConfig = {
        EnvironmentFile = "/etc/goatcounter-proxy.env";
        SupplementaryGroups = [ "keys" ];
        DynamicUser = true;
        Restart = "always";
        ExecStart = lib.escapeShellArgs [
          (lib.getExe cfg.package)
          "proxy"
          "-site=${cfg.proxy.site}"
          "-listen=${cfg.proxy.listenAddress}:${toString cfg.proxy.port}"
          "-ratelimit=10/1"  # 10 requests per second per IP
        ];
      };
    };
  };
}

For each server, I enable the local proxy with the following snippet. The nginx configuration shown earlier exposes the /count endpoint under the same domain as my blog.

{
  luffy.goatcounter = {
    proxy = {
      enable = true;
      site = "goatcounter.luffy.cx";
      listenAddress = "127.0.0.3";
      port = 8087;
    };
  };
}

Backup of the SQLite database with Litestream

Litestream is a streaming replication tool for SQLite databases. It compresses the changes committed to the write-ahead log (WAL) next to the database and sends them to a remote destination. I encapsulate its configuration in a NixOS module, which takes an attribute set databases mapping a name to the path of the database to back up.

Litestream also runs in a container. I mount the databases to replicate, as well as the secrets to push the backups to a Hetzner storage box using SFTP:

{ config, pkgs, lib, ... }:
let
  cfg = config.luffy.litestream;
  databaseDirs = lib.unique (map builtins.dirOf (builtins.attrValues cfg.databases));
in
{
  config = lib.mkIf (cfg.databases != { }) {
    luffy.containers.litestream = {
      mounts = databaseDirs;
      keys."litestream.env" = [
        "${pkgs.runtimeShell}"
        "-c"
        "pass show personal/nixops/secrets | grep '^SQLITE_BACKUP_'"
      ];
    };
  };
}

Inside the container, I configure Litestream through NixOS’s services.litestream options:

  • full snapshots every day, kept for 15 days,
  • three levels of compaction for transaction files: 5 minutes, 30 minutes, and 3 hours,
  • auto-recovery,8
  • replica stored in a directory matching the host name, and
  • credentials read from /etc/litestream.env and exposed through variable expansion.
{ config, pkgs, lib, ... }:
let
  cfg = config.luffy.litestream;
in
{
  config.luffy.containers.litestream = {
    config = {
      # The databases belong to dynamically allocated users, whose UID is
      # not known here, so Litestream runs as root.
      systemd.services.litestream.serviceConfig = {
        User = lib.mkForce "root";
        Group = lib.mkForce "root";
      };
      # Use NixOS service.
      services.litestream = {
        enable = true;
        environmentFile = "/etc/litestream.env";
        settings = {
          auto-recover = true;
          snapshot = {
            interval = "24h";
            retention = "360h";
          };
          levels = [
            { interval = "5m"; }
            { interval = "30m"; }
            { interval = "3h"; }
          ];
          dbs = lib.mapAttrsToList
            (name: path: {
              inherit path;
              replica = {
                type = "sftp";
                host = "\${SQLITE_BACKUP_HOST}";
                user = "\${SQLITE_BACKUP_USER}";
                password = "\${SQLITE_BACKUP_PASSWORD}";
                host-key = "\${SQLITE_BACKUP_HOSTKEY}";
                path = "${config.networking.hostName}/${name}";
              };
            })
            cfg.databases;
        };
      };
    };
  };
}

To back up GoatCounter’s database, I declare a goatcounter attribute in luffy.litestream.databases, set to the database path:

{ config, ... }:
let
  cfg = config.luffy.goatcounter.serve;
in
{
  luffy.litestream.databases.goatcounter = cfg.databaseFile;
}

On the SFTP server, we can inspect Litestream’s work, with the compacted transactions and the full snapshots:

❯ ls web02/goatcounter/ltx
web02/goatcounter/ltx/0
web02/goatcounter/ltx/1
web02/goatcounter/ltx/2
web02/goatcounter/ltx/3
web02/goatcounter/ltx/9
❯ ls -lh web02/goatcounter/ltx/1
29.1K Sep  5 01:25 0000000000003f2a-0000000000003f2b.ltx
72.4K Sep  5 02:03 0000000000003f2c-0000000000003f2d.ltx
63.3K Sep  5 02:24 0000000000003f2e-0000000000003f2f.ltx
[…]
❯ ls -lh web02/goatcounter/ltx/9
 8.5M Sep  5 02:00 0000000000000001-0000000000003f2b.ltx
 8.5M Sep  6 02:03 0000000000000001-0000000000004008.ltx
 8.6M Sep  7 02:03 0000000000000001-00000000000043a8.ltx
[…]

We can restore the database from the backup with a few shell commands. First, we stop the containers. Then, we move the damaged database away, invoke litestream restore from the right environment, and restart the containers.9

# systemctl stop container@goatcounter container@litestream
# mv /var/db/goatcounter/db.sqlite{,.old}
# ( . /etc/nixos-containers/litestream.conf ; 
>   set -a ; . /var/keys/litestream.env ; set +a ;
>   $SYSTEM_PATH/sw/bin/litestream \
>     restore -config $SYSTEM_PATH/etc/litestream.yml /var/db/goatcounter/db.sqlite)
# ls -lh /var/db/goatcounter/db.sqlite
-rw-r--r-- 1 root root 20M Sep 20 07:33 /var/db/goatcounter/db.sqlite
# systemctl start container@goatcounter container@litestream

Ten years after removing Google Analytics, JavaScript-based analytics is back on this blog, but without storing cookies or IP addresses, and without involving a third party. I still write for myself first, notably because it lets me dig into a topic and refer back to it years later. But knowing a bit more about my fellow human readers is a nice bonus, even the ones disabling JavaScript. 🐐


  1. Nginx scrambles IP addresses before storing them, thanks to the ipscrub nginx module. ↩

  2. GoatCounter already filters some bots based on the user agent or the IP address. But AI scrapers lie about their user agent and hide behind residential proxies. ↩

  3. Without JavaScript, I cannot send the real referrer. I insert “NoJS” instead. ↩

  4. I am missing the humans reading the RSS feed. I am not comfortable adding a tracking pixel, and bots are likely to fetch it, compromising the statistics. I’ll live without counting these readers. ↩

  5. In February 2025, I tested another CSS-only approach without the interaction trick: each page loaded an empty image as a background. Over a month, it recorded up to ten times fewer page views than the nginx logs, even after removing user agents identifying as bots. Compared to this experiment, GoatCounter counts about three times fewer views, 18 months later. As the time ranges differ, this is not an apples-to-apples comparison, but it gives a hint about how efficient bot filtering is. ↩

  6. A NixOS module is a function receiving the configuration of the whole system as config and returning three attributes:

    • imports adds other modules to import,
    • options declares user-configurable settings, each with a type and a default value, and
    • config sets values for options declared by any module, like containers from NixOS.

    If a module does not need to declare options, you can return the config attribute set directly. When a module does not require any argument, you can define it as an attribute set instead. ↩

  7. Most of the time, you don’t need an explicit import. NixOS automatically imports the modules shipped with Nixpkgs. For my own modules, some machinery also imports them automatically. ↩

  8. Litestream warns against using the auto-recover option as it can cause data loss. But I don’t properly monitor my servers and I prefer uninterrupted backups to a slight chance of losing the last few records. ↩

  9. In my case, the process is slow: around 20 minutes for a 20 MiB database. You can test by restoring to a copy with the -o option, but you still need to stop the Litestream container. ↩

  •  

Andrew Cater: Debian 11 is at end of life from Long Term Support

20 September 2026 om 17:04

 Lots of posts in the debian-user mailing list complaining about updates with Debian 11.11 suddenly failing.

See Debian 11 Long Term Support reaches end-of-life

August 31st, 2026

The Debian Long Term Support (LTS) Team hereby announces that Debian 11 bullseye support has reached its end-of-life today, 31 August 2026, five years after its initial release on 14 August 2021.

Starting in September, Debian will not provide further security updates for Debian 11. A subset of bullseye packages will be supported by external parties. Detailed information can be found at Extended LTS.

The Debian LTS Team is currently providing security support for Debian 12 bookworm, the current oldstable release. Thanks to the combined efforts of different teams including the Security Team, the Release Team, and the LTS Team, the Debian 12 life cycle encompasses five years. Debian 12 will receive Long Term Support until 30 June 2028. The supported architectures in Debian 12 LTS are amd64, i386, arm64, armhf and ppc64el.

For further information about using bookworm LTS and upgrading from bullseye LTS, please refer to LTS/Using.

Debian and its LTS Team would like to thank all contributing users, developers, sponsors and other Debian teams who are making it possible to extend the life of previous stable releases, and who have made Bullseye LTS a success.

If you rely on Debian LTS, please consider joining the team, providing patches, testing or funding the efforts.

 

  •  

Kentaro Hayashi: Building with dh-bazel, buildsystem support for debhelper experiment updates

19 September 2026 om 10:33

Introduction

After bazel-bootstrap 7.7.1 was landed into Debian unstable, I'm working on packaging newer Mozc (Most famous Japanese input method editor) with Bazel.

Here is the blog entry initial efforts to build Mozc with Bazel at that time.

kenhys.hatenablog.jp

Then, I've shared implementing PoC dh-bazel experiment. See why dh-bazel is needed, and prototype about dh-bazel.

kenhys.hatenablog.jp

As a Bazel beginner, want to know what should pass to Bazel, what should not pass to Bazel.

What is improved in recent dh-bazel?

In the previous versions of dh-bazel, it supports only basic features to build with Bazel as a thin wrapper.

%:
        dh $@ --buildsystem=bazel

override_dh_auto_build:
        dh_auto_build -- //:hello

Now, with recent changes, it supports the following environment variables to resolve required system libraries in dynamically.

salsa.debian.org

  • DH_BAZEL_OVERRIDE_MODULE

It search the specified modules from bundled dummy modules in dh-bazel. It accept ',' separated paramesters. (e.g. DH_BAZEL_OVERRIDE_MODULE=zlib,zstd)

dh-bazel bundles abseil-cpp, apple_support, buildozer, lz4, openssl, protobuf, rules_android_ndk, rules_apple, rules_swift, zlib and zstd as dummy modules to linking system libraries.

Now you can use it in debian/rules like this:

#!/usr/bin/make -f
# -*- makefile -*-
#

export DH_BAZEL_OVERRIDE_MODULE=zlib
export DH_VERBOSE=1

%:
        dh $@ --buildsystem=bazel --without=single-binary

override_dh_auto_build:
        dh_auto_build -- //:hello

It is impossible to cover all of system libraries in Debian, so in that case, please consider to use the following DH_BAZEL_PKGCONF_MODULE.

  • DH_BAZEL_PKGCONF_MODULE

It search the specified modules with pkgconf. It is useful when there is no bundled modules in dh-bazel if you want. It accept ',' separated paramesters. (e.g. DH_BAZEL_PKGCONF_MODULE=gtk4-x11,gtk4-unix-print).

If DH_BAZEL_PKGCONF_MODULE could not match, then dh-bazel fallback to dig into Build-Depends: field in debian/control. Note that if it is not deterministic (e.g. -dev package provides multiple .pc files) dh-bazel gives up fallback with pkgconf.

Now you can use it in debian/rules like this:

#!/usr/bin/make -f
# -*- makefile -*-
#

export DH_BAZEL_PKGCONF_MODULE=gtk4-x11,gtk4-unix-print
export DH_VERBOSE=1

%:
        dh $@ --buildsystem=bazel --without=single-binary

override_dh_auto_build:
        dh_auto_build -- //:hello

Conclusion

dh-bazel is still in very early stage prototype, but it resolves some sort of packaging glitches a bit by bit.

I hope that it will help package maintainer using Bazel. (dh-bazel is not uploaded into debian/unstable yet, so stay tuned!)

  •  

Ludovic Rousseau: New version of PyKCS11: 1.5.20

4 September 2026 om 19:01

I just released a new version of PyKCS11, a Python wrapper above the PKCS#11 API.

See PyKCS11 introduction or PyKCS11’s documentation.

The project is registered at Pypi: https://pypi.org/project/PyKCS11/

Explanation

This version only fixes a problem on Windows.

In version 1.5.19, I upgraded the PKCS#11 header file pkcs11.h to version PKCS#11 3.2. Execution of automatic tests failed on Windows. I then disabled failed test. But I, in fact, disabled all the tests on Windows. And the problem went unnoticed.

Version 1.5.19 did not contain the code required to pack the structures on Windows. So the PyKCS11 wrapper and the PKCS#11 library were not alligned on the data format. And crash...

Changes:

1.5.20 - September 2026, Ludovic Rousseau

  • fix C_Initialize() crash on Windows

  •  

Ludovic Rousseau: Security issues reported by an AI tool in CCID driver

29 Augustus 2026 om 15:39

version 1.8.3 of the CCID driver (New version of libccid: 1.8.3) addresses 6 security issues identified by an AI tool.

Very low impact

The reported issues are either not exploitable because the input and output buffers used by PC/SC Lite to call the CCID driver are large enough, or the exploitation requires rogue smart card or smart card reader (i.e. sending non-compliant data).

If you would like to read the details, the issues have been fixed in the following git commits:

Comments

The AI tool identified real issues.

However, it would be difficult to exploit these issues unless the attacker used a custom-built reader or smart card. This is something I started doing with the Pico HSM project, which is a CCID reader in a Raspberry Pi Pico. This allows you to modify the CCID frames sent by the reader as required.

The bug reports from the AI tool are very verbose. This is useful for impressing a manager with a long, complex-looking text. However, reading the entire report is often a waste of my time. It is often much faster to read the proposed patch to understand the problem it is trying to fix.

The proposed fixes are, sometimes, incorrect. They are a good starting point, though. However, never apply an AI-generated patch without fully understanding it.

Conclusion

Thanks to Red Hat and Jakub Jelen for the bug reports.

I am very happy that the AI tool only identified issues with no or low impact.

  •  

Ludovic Rousseau: New version of libccid: 1.8.3

29 Augustus 2026 om 15:32

I have just released version 1.8.3 of libccid the Free Software CCID class smart card reader driver.

Red Hat reported 6 security issues found by an AI tool. See Security issues reported by an AI tool in CCID driver.

Changes:

1.8.3 - 29 August 2026, Ludovic Rousseau

  • Add support of

    • Broadcom Corp 58200 0x5884

    • Broadcom Corp 58200 0x5885

    • Broadcom Corp 58200 0x5886

    • Broadcom Corp 58200 0x5887

    • Circle CIR135 ICC

    • DigiFlow LLP. KAZTOKEN

    • HID Global Crescendo NFC Reader

    • HID Global OMNIKEY Plug

    • HID Global OMNIKEY SE Plug

    • Neowave LinkeoC-PRO

    • Swissbit iShield Key 2 Pro

  • macOS: provide a sample script to build the driver with meson

  • fix some minor issues found by an AI tool

  • Some other minor improvements

  •  

Paul Tagliamonte: Why Write?

18 September 2026 om 19:05

As a graduate of a liberal arts university, I wound up, unsurprisingly, taking a lot of classes in every possible academic discipline. Thinking back to the person that I was going into university, I don’t think I would have chosen to take them – after all, my degree was in the sciences; I’d have been stoked to do nothing more than wall-to-wall computer science until I ran out of coursework and filled the rest of my hours with research and independent studies with my professors (which, I guess I did actually do, just not as much as I would have otherwise).

Even this blog’s CSS theme (now 18 years old and starting to look it) was something I wrote after receiving and repeatedly re-reading a tattered third-hand copy of “The Laws of Thought” by George Boole. It was gifted to me by a college friend majoring in philosophy while I was crashing at his rental on the beach. He gave it to me because he knew I “liked that shit” and, while it was definitely computer science in nature, I would not have read it otherwise. I have vivid memories of sleeping on his la-z-boy surrounded by towers of books he was working through. He went on to do incredible work, getting his PhD, doing research, brilliant writing – his death in 2023 has robbed humanity of more time with him. “The Laws of Thought” sits behind me in my office, and is one of my most valued possessions.

My friends mean more to me than I could ever express, and I definitely don’t show it enough. Each of my classes made me a more thoughtful person. My professors made me a better, more well-rounded person – and a person who attempts to live up to the oft repeated credo of being “men and women for others”. I did the best I could, even though I was never a particularly good student. Without liberal arts, I don’t think I would, dispositionally, have been capable of pushing myself – to this day – to continue to learn on nights and weekends, for no reason other than wanting to learn. I refuse to stop expanding my perspective, and do my best to approach new problems with as much humility and curiosity as I can muster.

Every few days for the last year or so, I have been thinking back to a reading assignment from my junior year that, at the time, I thought was a borderline throwaway filler assignment for the class. The reading is an essay from 1947 by the french existentialist philosopher and libertarian marxist, Jean-Paul Sartre (as some of the more erudite may have now already worked out given the blog’s title), “Why Write?”. I re-read it this week. It is not filler. I remember this essay better than some of the classwork I considered more important at the time.

Why Write?

“Why Write?” starts off by describing the ways in which writing – trying to communicate your thoughts, opinions, or feelings to others – is an act of projection. The writer will only ever draw from a place of their “own subjectivity” – writing is taking your person and putting it on display. You’re choosing what words to use, how to use them, pulling from knowledge you’ve accumulated (based on how you’ve chosen to spend your time). Spending any amount of your finite existence in order to convey a thought is, itself, announcing to the world that you believe it to be a thought worth sharing.

Meanwhile, the act of reading is not simply turning letters into words. Reading is engaging with the work, and understanding the work in a process that looks more like what Sartre terms “re-invention” or “discovery” – “the literary object, though realized through language, is never given in language”. It is not enough to read the words in a book end-to-end; you must, as a reader, actively engage with the work to understand what is being communicated by those words. Reading is to take the words on the page, and “exceed” the mere words through what he calls “directed creation”. The reader is “re-inventing”/“discovering” the writer’s thoughts by following their “landmarks in the void”.

And this all makes pretty good sense to me – I know if someone is being sarcastic because I know the writer; I have read their words and have read into their words, allowing myself to be directed by the writer into “discovering” the thought they have left me in their work. I understand that their words are humorous, not irate. Knowing who the author of a work is can completely change the point of a sentence. The fact I’m writing about Sartre at all, or that this very writing has been constrained for presentation in this blog’s CSS is only happening because of who I am, what things I’ve experienced in life, and what has made me, me. Sartre argues that this dyadic coupling between writer and writing means that I, as a writer, can never truly read my own writing. I will never be capable of reading my own work and getting something out of it – the work is already an extension of my own person. I can discover no thought in my own works.

Of course, Sartre, as an existentialist, is honor bound to go one step further here – the reader, by participating in reading a work, is asserting their own freedom. Every time a writer writes “[…] the writer appeals to the reader’s freedom to collaborate in the production of [their] work”. The creative act may only be complete if both writer and reader have recognized one another and made a choice to do so. No one is compelled to complete the creative act. The things you read matter. Reading something fundamentally alters you as a person – you can not simply un-read a thought. Everything you read changes your universe. I have a hunch Sartre would find things like the (original early 2010s era) “tl;dr” reply to obviously terrible writing absolutely hilarious as a reader’s expression of freedom (not to be confused with modern 2020s era usage as shorthand for “summary section”). Using your freedom to choose to complete the writer’s creative act (or not) is inherently asserting your humanity.

I’m a bit fuzzy on the specifics of this quote, but I think it was sj who told me at one year’s Mystery Hunt that “A puzzle is a contract between puzzle author and puzzle solver, the author promises that the puzzle is solvable if you’re clever enough”. The puzzle author and puzzle solver are engaging in a collaborative production of the work that is only possible by recognizing one another. Similarly, Sartre – “whatever connections [the reader] may establish among the different parts of the book among the chapters or the words [the reader] has a guarantee, namely, that they have been expressly willed”.

Wall Drawing #123

It follows, Sartre argues, that the act of writing, as a means to convey a thought to a reader, may only be completed through the act of reading. Reading can only be done by others; writing, therefore, only exists to be read – it can serve no other purpose. Writing and reading are two halves of the same creative, collaborative act, only satisfied when both writer and reader acknowledge one another. Combined, writing and reading are the act of recognizing one another’s humanity, thoughts, experiences, consciousness – the act of co-creation of thought is the critical aspect of writing and reading – reconstructing the author’s perspective, thought, intent, point. The co-creation is the point of both reading and writing.

Writing without anyone to read the work leaves the writer’s bid for co-creation unsatisfied and humanity unrecognized. No thought has been conveyed to any reader, no one has understood the reason behind the “landmarks in the void” you’ve carefully placed, leaving you to either “[…] put down [your] pen or despair”. Writing is an appeal to the reader’s freedom, and the nature of that freedom is the writer can not control it – never being understood is always a possibility any time anyone sets out to write.

Nearly 12 years ago, I attempted to read the text on the git manpage generator for the first time – I remember the exact feeling of my brain going into “git manpage parsing mode” where every word was dragging git plumbing megaliths through the sand from their far-flung homes. I can still feel the surface of my desk as I instinctually started to trace out logical connections between git internals referenced as I read along. It took me a good 20 seconds to realize what I was reading made no sense. This website broke Sartre’s writer-reader agreement – I was attempting to read this website, but there was nothing there. You can “mechanically” “read” the git-man-page-generator, but you can never read it – it is not possible to read it. It was funny – unsettling. Each grasp at a real-looking image in front of me misses, my hand coming back empty. I couldn’t get enough of it. I must have tried to read a dozen generations in a row. I had never had something quite that broken pass that far into my consciousness before. Although I kept trying, it never did feel quite the same as that first time; I think fundamentally, I couldn’t forget that it was random.

While never having anyone read your writing drives them to “put down your pen or despair”, Sartre never had to contend with the opposite problem – being asked to read that which was not written. Reading that which was not written does not merely leave someone unrecognized when a reader makes a choice; instead, it is an inherent violation of the contract between reader and writer. Not only is no humanity being recognized through attempting to read words which were not written, but the reader, not the writer, is the one who bears the burden of unexpected apophenia. The reader, while engaging in co-creation, must now contort themselves to uphold their end of the Sartrean agreement, scrutinizing text to ascribe consciousness, thought and intent only to come back up with pareidolia-fueled echos of one’s own self and disfigured half-thoughts of others. The reality is, the modern reader must now “mechanically” “read” most written work they come across, scrutinizing text for any signs of thought before truly attempting to read the work. Failing to do this correctly changes our person – each time it happens, a small part of us is irrevocably altered. Choosing to engage in this ballet because you wish to do so is one thing – this is your right and freedom as a reader – but passing words which were not written as your own in an attempt to wrestle a reader’s freedom of co-creation from them is another entirely.

If you didn’t write it, I don’t want to read it (dw;dr). Send me your prompt instead.

  •  

Petter Reinholdtsen: Long term storage of Mattermost messages with Noark 5 XML

18 September 2026 om 09:00

It is said that those who cannot remember the past are condemned to repeat it, a quote often attributed to the American philosopher George Santayana. And to remember the past, records must be maintained and kept accessible to learn from. With this in mind, it is no wonder that archivists worldwide consider it crucial to ensure the archival records are both complete and accurate, with a constant frustration caused by the knowledge that the archives are neither due to challenges in tracking down and collecting what should be archived.

The last few days, I decided to simplify the collection of Messages from the Mattermost chat service, used by a few of the organisations I am involved in, to try to improve the situation slightly. To achieve this, I had written a dedicated extraction tool. Partly to see how hard it would be, and partly to ensure that if one of the services were shut down or replaced, everything in it would not be lost. Of course it is in the nature of the use of instant messages that most of them are not fit for permanent storage, at least not according to Norwegian law, where there is a threshold "worthy of the archive" (arkivverdig) that should be met, and messages like "should we go to lunch now" are below the bar and should be filtered out. The tool can not help with this filtering, and that will have to be done using other means after collection.

The tools I had my bullshit generator write, under strict supervision and many iterations, will extract every message visible to the user whose credentials are used to log into Mattermost, and write it out in Noark 5 extraction XML format and visualize the result. I created the visualiser mostly to quickly be able to debug the extracted XML, but also to make life easier for anyone interested in testing out the tool set.

The extractor mattermost-noark5extract create a hierarchy with arkiv/arkivdel for the Mattermost server, and then individual mappe for each channel and direct message chat, a registrering for each message thread, dokumentbeskrivelse for every message in the tread, and one or more dokumentobjekt for each message and their attachments/images. So far it is only tested on one Mattermost installation, where around 23,000 messages is extracted in 61 seconds and produce 295M with approximately 600 attachments and around 675,000 lines of XML in arkivstruktur.xml. You pass it the URL of the service, a username and password, a directory path where to store the collection and an optional channel name substring to limit the collection to only a subset of the messages available.

The mattermost-noark5extract-browser viewer can load this collection and visualize it similarly to Mattermost's web interface, with the list of channels and direct messages dialogues on the left, messages chronologically in the centre and a selected thread displayed on the right.

I know Mattermost provide several login options. I've only had the one used on my test server implemented so far, and know the extract tool will have to be extended a bit for it to handle servers using one of these options.

If you would like to examine the new toolkit, mattermost-noark5extract is available from gitlab. I wanted to put it on codeberg, but as the recent rule change forbid code mostly written by a bullshit generator there, it was not really an option. Note that some years ago I wrote a similar tool to extract material from the request tracker system. The source for request-tracker-noark5extract is also available from codeberg. I would love to hear from you if you test the tools.

As usual, if you use Bitcoin and wish to support my activities, please send donations to 15oWEoG9dUPovwmUL9KWAnYRtNJEkP1u1b.

  •  

Louis-Philippe Véronneau: Fake Cash: The World Trade Game

17 September 2026 om 23:45

This is a new series1 I'm starting on the tools I have been crafting and buying to play pedagogical games in my Economics classes.

These games, aside from being just plain fun for the students, often aim to teach them a lesson via interactive play. Unsurprisingly, it is much easier for them to grok a particular concept this way. Moreover, many students told me these activities were the best part of the classes I teach.

The World Trade Game

I found out about The World Trade Game in The Handbook of Pluralist Economics Education2, which also contains many other fun pedagogical games. It was first developed by Action Aid, but has since been refined by John Sloman and then by Irene van Staveren, who wrote about her use of the game in the aforementioned book.

The goal of this blog entry is not to describe the game and teach you how to play it. If that interests you, either buy the book I mentioned or read about Sloman's version here.

No, our focus point here is the tools to play this game: fake cash! Fake cash can be used in many economics pedagogical games and in my opinion, is a worthwhile "investment" of your time and energy. You could indeed play with regular printed sheets of paper listing a value (I sure did!), but using Play Money (a more appropriate term, since we're not trying to defraud anyone) will up your game and create a more immersive atmosphere in your classes.

At first, I tried to buy play money. Surely, some commercial offering aiming to create realistic props for movies and TV productions existed? At least in Canada, it turns out the answer was no. The only thing I could find were shady websites and weird expensive listings on AliExpress. Not being tenured yet, I did not try to convince my employer to take a 50-50 chance on supporting criminal endeavours.

As such, the next best thing I found was printable play money designed by the Bank of Canada Museum. Their designs were quite nice and met my needs, but their PDF layout made it overly complicated to print large quantities of bills. As such, I played around with GIMP a little and came out with these files. A commercial printer shouldn't have issues printing them on glossy cardstock paper and cutting them up for you:

Once printed and cut, you'll unfortunately have to fold each bill by hand. To secure both halves, I ended up using a strip of transparent tape on each long edge.

Here is the result, with a real Canadian 20$ bill as a comparison:

Stacks of play money bills, with examples of the 5$, 10$, 20$, 50$ and 100$ bills

A real Canadian 20$ bill, side-to-side with the play money version


  1. Don't hold your breath though, as I do not currently have a release schedule. ↩

  2. Reardon, J. (2009). The Handbook of Pluralist Economic Education. New York : Routledge. ↩

  •  

Kentaro Hayashi: Building Mozc with dh-bazel, buildsystem support for debhelper

17 September 2026 om 14:53

Introduction

After bazel-bootstrap 7.7.1 was landed into Debian unstable, I'm working on packaging newer Mozc (Most famous Japanese input method editor) with Bazel.

Here is the blog entry initial efforts to build Mozc with Bazel at that time.

kenhys.hatenablog.jp

Why dh-bazel?

After that, newer Mozc packages are uploaded into experimental and moved to testing phase on experimental now.

mozc - Debian Package Tracker

When started packaging efforts for newer Mozc with Bazel, I'm a newbie to do it. Now I've got a knowledge to do it a bit, I want to know best practice on packaging X on Debian with Bazel furturmore.

Usually there are dh-X for buildsystem X on Debian, but it's not true for Bazel as far as I know. This is why I had started to write dh-bazel.

I've wrote initial dh-bazel prototype and post a mail to debian-bazel ML.

lists.debian.org

dh-bazel supports the following way:

%:
        dh $@ --buildsystem=bazel

override_dh_auto_build:
        dh_auto_build -- //:hello

If you want to build source under src, you could write d/rule like this:

%:
        dh $@

override_dh_auto_build:
        dh_auto_build --buildsystem=bazel --sourcedirectory=src -- //:hello

Conclusion

dh-bazel is in very early stage prototype, but I have succeeded to build newer Mozc with some modifications to Mozc debian/rules on experimental with dh-bazel locally!

dh-bazel is a thin wrapper for Bazel, so it does not reduce packaging glitches dramatically, but it helps some sort of packaging tasks IMHO.

There are some achievement with dh-bazel

  • It helps to do similar building way like other debhelper buildsystem
  • It sets comprehensive Bazel startup options
  • It sets comprehensive Bazel command options
  • If you want to set extra flags, just specify it as additional user options (--override_module=, and so on)

Note that it only simplify dh_auto_build stage, so you must manually install artifacts with .install or something correctly.

I hope that it will help package maintainer using Bazel in the future. (dh-bazel is not uploaded into debian/unstable yet, so stay tuned!)

  •  

Russell Coker: Nheko DBUS

17 September 2026 om 03:40

Nheko is my current favourite client for the Matrix IM system, which is my favourite IM system.

Matrix is an open system with end to end encryption and Nheko is free software and runs well on Linux desktops and phones.

The Nheko client allows interaction with dbus which could be good for automating things, EG you could change the status message when unlocking the screen. I’m documenting the most useful ones here because they don’t seem to be documented anywhere else. I have filed a Debian bug about the activate room option not working. The qdbus6 program is the QT6 version of the dbus command-line query program, there are a range of other programs which work in much the same way.

# list all interfaces
qdbus6 im.nheko.Nheko / 
# get the version of Nheko
qdbus6 im.nheko.Nheko / im.nheko.Nheko.nhekoVersion
# list rooms in a dump of the data structures (pity it's not json or something)
qdbus6 --literal im.nheko.Nheko / im.nheko.Nheko.rooms|less
# join a room
qdbus6 im.nheko.Nheko / im.nheko.Nheko.joinRoom "#flounder-random:luv.asn.au"
# supposed to activate a room but doesn't
qdbus6 im.nheko.Nheko / im.nheko.Nheko.activateRoom "#flounder-random:luv.asn.au"
# set the status
qdbus6 im.nheko.Nheko / im.nheko.Nheko.setStatusMessage "whatever"
# get the status
qdbus6 im.nheko.Nheko / im.nheko.Nheko.statusMessage

Here are a couple of examples of using other dbus clients to get similar results. Note that the difference between the Debian version of Nheko (and maybe other recent versions) and what LLMs return for usage examples is that Debian has “/” as the path while the examples have “/im/nheko/Nheko”.

# list rooms via gdbus
gdbus call --session --dest im.nheko.Nheko --object-path / --method im.nheko.Nheko.rooms
# get status via dbus-send
dbus-send --session --print-reply --type=method_call --dest=im.nheko.Nheko / im.nheko.Nheko.statusMessage

Related posts:

  1. dbus error Strange dbus error from the KDE power monitoring tool...
  2. Hangouts Replacement Google is currently in the process of killing Hangouts. Last...
  3. Matrix Introduction In 2020 I first setup a Matrix [1] server....
  •  

Paul Tagliamonte: DESFire EV3

15 September 2026 om 15:25

I’ve long been interested in hardware key material storage devices. I’ve been a fan of yubikeys (I still remember when my fancy new NEO-N showed up), PIV (and its associated smattering of additional fields), SaaS HSMs, the kernel keyring, some tooling I’ve fairly satisfied with the design of at prior companies, and of course, our dear friend, the TPM. All that is not even to mention the scores of exotic hardware security modules one generally comes across from time to time when you’re keeping a sharp eye out that you wind up playing with.

The concept of storing private key material on a disk, or even having it in RAM has always skeeved me out, so I have a natural inclination to hardware modules, and how shifting keying material around can change your risks and threat model(s) in interesting ways.

I don’t remember when I first came across the MIFARE DESFire EV3, but a few weeks ago I did a deep-dive into the state of the art of authentication schemes using ID cards. My complete overview of what tradeoffs exist is pretty extensive (and likely not interesting to the vast majority of the world), but the tl;dr wound up being one of “use PIV” or “use MIFARE DESFire EV3”. I wound up picking DESFire for a recent project, and figured it’s worth talking a bit about what I learned, share some thoughts, and some code. That code is published on crates.io/desox, and docs, as is our custom, may be found at docs.rs/desox

DESFire supports DES (I’m sure most readers saw that one coming), 3DES (I didn’t bother playing with 3DES at all) or AES-128 (AFAICT always use this?) keying material. It’s worth noting that the DESFire only supports symmetric keys and is not designed for public key cryptography, and operates exclusively using shared symmetric key material. The DESFire EV series use those keys and related authentication schemes to interact with “files” stored on the on-chip EEPROM (2k, 4k, 8k, and 16k versions exist), or “applications” (groups of files and authentication keys).

Talking to a DESFire EV3

Interactions with the card are done over NFC (ISO/IEC 14443 Type A), and commands to/from the card may be in the usual ISO/IEC 7816-4 APDU format, or “unencapsulated” bytes sent to/from the card are sent using a fixed instruction set and return code structure – saving a few bytes per message. I’ve opted to use their undocumented and proprietary format – I found it easier to work with and with a maximum message of 60 bytes, the savings matter a lot.

While powered via NFC, the card maintains a small amount of state about the connection between the reader and the card in its RAM, including if the session is authenticated or unauthenticated. I’ll dig into how authentication happens later, but it’s worth knowing that sessions can become authenticated using one of the symmetric keys shared by the card and the reader. The vast majority of the DESFire commands I know about tend to work while either authenticated or unauthenticated, with a few exceptions (GetUid, ChangeKey, and ChangeKeySettings for example).

In general, I found working with this card particularly pleasant. There is a fair amount of backwards-compatible behavior and multiple methods of communication that confuse things a bit, but overall, it was better than average to integrate with. Kudos to the NXP team. If the docs on this chip were public, things would be orders of magnitude easier – it’s not entirely clear to my why they’re keeping so much of the interface documentation under NDA, but it’s the largest knock against the chip, by far.

Authentication

I found a lot of really great resources outlining how the handshake and protocol works for a DESFire EV3, especially from Ridrix, some public datasheets ThrRealRevK and posts from AndroidCrypto.

The gist here is that, because the DESFire only does symmetric key operations, the key exchange (a type of SKA – Symmetric Key Agreement) uses symmetric keys to establish a unique session key which is used to sign or encrypt data exchanged between the reader and the card. I’m not going to get too in-depth here, since there’s a ton of other resources out there to dig into – but I will do a quick high-level description to keep this post mostly self-contained.

The authentication protocol serves two main functions – to verify that both parties know the same shared secret, as well as to act as a SKA to construct a new session shared secret key. Here’s a quick overview of how a shared session key is derived between the reader and the card using our symmetric keys (AES-128 in the case below).

  1. the reader requests to start authentication with the card (something like AA 00 to start an AES Authentication handshake with keyslot 0x00).
  2. The card will then reply with AF (a status code that indicates more data is to follow), followed by 16 bytes (in the case of AES-128) of encrypted (using CBC) data.
  3. The hosts then decrypts this block with the symmetric key from keyslot 0, returning the card’s session nonce.
  4. The host generates 16 bytes (usually random) for its session nonce.
  5. The host sends an instruction of AF (indicating a continuation of the previous command), followed by 32 bytes of encrypted data. When decrypted, the first 16 bytes are our nonce generated in step #4, followed by the 16 bytes provided by the card, decrypted in step #3, except where every byte is shifted to the left by one place (the 0th byte is copied to the end).
  6. The card will reply with 00 indicating a successful operation, followed by 16 bytes, which when decrypted, is our session nonce from step #4, shifted to the left by one byte in the same way that we did in step #5 with the card’s nonce.
  7. At this point, both the reader and card have confirmed the other party has the same symmetric secret key. The session is now “authenticated” and a “session key” is derived using the two nonce blocks. Two hashing keys (K1 and K2) are derived from this key, which is used to maintain an ongoing CMAC hash of the messages coming and going to/from the card.

From here on out, the session is “authenticated”, and responses from the card which were previously “plain” will now contain a 8-byte CMAC signature, which can be used to ensure that the replies in question come from the active session.

In my implementation of the handshake I opted to encode the handshake state into rust types, just so I wouldn’t make any mistakes. The Handshake type contains the session internals (session nonce values, keying state, to include IV, etc). This means the authentication flow (from within my code) uses the Handshake struct to generate the commands to send to the card in order:

/// Create a new `Handshake`, and return the
/// start auth command (something like `AA 00`)
fn Handshake::<Initial>::begin(
 output: &mut [u8],
 key: [u8; 16],
 key_id: u8,
) -> (Self, &[u8]);

After we get a reply back from the card (the encrypted version of the card’s session nonce, sometimes called Rnd_B in code I’ve seen), we transition states from Initial into HalfOpen.

/// Given the card's encrypted response, generate
/// our session nonce and generate a reply
/// (something that starts with `AF` followed by
/// 32 bytes of encrypted data).
fn Handshake::<Initial>::rnd_b(
 self,
 output: &mut [u8],
 input: &[u8]
) -> (Handshake::<HalfOpen>, &[u8]);

Now that we’re “HalfOpen”, we’re waiting to hear back from the card to ensure that it, too, can byte-shift our provided nonce. Once we have the card’s reply, we can check it using our complete helper, transitioning from HalfOpen to Successful.

/// Check to ensure that the card replied with
/// our nonce byte-shifted by one place, indicating
/// that they know the symmetric secret in
/// this key slot.
fn Handshake::<HalfOpen>::complete(
 self,
 input: &[u8]
) -> Handshake::<Successful>;

Once the Handshake is successful, the only thing left to do is consume the Handshake struct and turn it into the shared session key by running it through the key derivation function.

/// Consume the `Handshake` struct and return the
/// new shared session secret key.
fn Handshake::<Successful>::into_key(self) -> [u8; 16];

From here on out we can use this session key for the remainder of our interactions with the card – signing messages from (and sometimes to!) the card, or encrypted messages to and from the card. This key is used in CBC block mode, where the session IV is updated with the last block of the encrypted data.

Unit Testing

A nice proprietary of the SKA scheme we’re using as part of DESFire is that the derived session key is actually deterministic if you control your nonce RNG (ok, actually, pretty true for most key agreements, but anyway), which means it is possible to capture traffic over the NFC interface, and “replay” the NFC I/O with cooked RNGs and ensure byte-identical messages and keys are generated. Within desox-rs this is called replay (I’m creative), and I’ve got a few replay sessions checked into VCS, which exercise a signficant amount fo the API surface. All were derived from an actual session with a real DESFire card, and can be updated with a live card and a --cfg flag.

Each replay file is a set of lines (request-response transactions), each containing two space-delimited hex encoded NFC messages. For instance, here’s an authentication handshake in replay format:

1a00 afc7bbd82ff8fefae8
afc6dab54df2278d2952d560821be7e4c3 007d9abe94a9b14748

The code that generated that exchange came from the test stored adjacent to that file – a handshake with the default DES key (all zeros), and an RndA value hardcoded to 32c28fdafd3960de.

let mut card = card
 .authenticate_with_rnd_a(
 0x00,
 Key::Des([0; 8]),
 Key::Des(hex_literal::hex!("32 c2 8f da fd 39 60 de")),
 )
 .await
 .unwrap();

Since the card’s RndB is similarly unchanging (I’m replaying this file every time), this will always derive the same session key, which means messages (including encrypted ones or CMAC signed responses) will be identical, as well. If you’re playing with the DESFire yourself, feel free to grab my replay files if you need a “known good” baseline.

By default this will run using the MockBackend, replaying each file – expecting a byte-identical request, and responding with the harcoded customary reply. If the code (or test!) needs to change, updating the tests is done by swapping the MockBackend out for a real one. Since I had to do this a bunch during development, running cargo test with RUSTFLAGS="--cfg desox_replay_rw" will, on run, overwrite the replay file(s) for the executed test(s), ensuring all line-protocol changes are explicitly caught and reviewed.

Observations

Most commands, even ones which require authentication, are transmitted without CMAC signature(s) or encryption. CMAC signatures from the reader to the card are not really used (except for writes to a file which specifies communication must be CMAC signed), ditto for encryption (although that one is used for key change operations, in addition to file writes on files that specify encrypted communication must be used). The vast majority of commands take a “plain” request from the reader, and return a CMAC signed response.

By my eye, this means that a malicious reader, or something otherwise capable of holding the card online after communication with an authentic reader is complete are able to execute privilaged commands (since one can simply ignore the CMAC signatures on responses), so long as the command doesn’t require the reader to provide CMAC signatures (or encryption), or allow the card to power down.

Fun with DESFire

I’ve played around a bit with ways to use the DESFire cards in interesting configurations, given what they’re capable of. Here’s some half-baked thoughts I had while mucking around with the cards – these are all poorly thought out sketches of some things we can do given the specific tradeoffs I see with the DESFire card. It’s also worth noting that I don’t have any of the actual documentation, and am not a cryptographic grown-up, so take these sketches with a massive grain of salt.

The first thing that came to mind when implementing this is how the authentication scheme can shift the boundary of what is and is not trusted (assuming good secure keying, and provided the key slots and card/application permissions are configured correctly). Rather than push the key material out to the machine connected to the NFC reader (“reader machine”), I instead tried turning the NFC reader and computer into something psuedo-untrusted by “merely” having it pass messages from the card to a trusted remote system (“remote machine”). This means that the “reader machine” is exchanging NFC data with the card, but that data is being decrypted, encrypted and processed by the trusted “remote machine” – the reader is unable to derive the session key.

For each of these, I wind up needing to authenticate – so there’s still a few latent risks, but these can mostly be mitigated by asking for a readbacks of any changed file(s), setting key permissions carefully, and requesting the card’s UID via the encrypted channel – all of which would require the symmetric secrets (which undermine the whole security model if comprimised).

This general construction is also subject to a hostile takeover of the untrusted “reader machine”, since most commands (including destructive ones!) are sent in “PLAIN” mode – the reader machine can wait until authentication is complete and then inject commands into the card and “simply” ignore the CMAC signatures on responses, severing ties with the remote machine. As such, we also need to take steps to ensure that the key being used is not one that allows any access beyond what is allowed. Here were some ideas I sketched out off the back of this theory.

The “second-factor”

Given some established (and authenticated) connection, part of the initial authentication flow may use the DESFire card to prove physical control over it as part of a handshake. This can serve as a second factor during some authentication flow, requiring physical card presence at a reader to fully initialize a connection. This does have one glaring downside, however – it’s phishable. To use this “for real”, we’d need to take some steps to prevent obvious MITM flows (XOR the NFC messages with the URI as seen by the client?), but maybe there’s something interesting there.

This also has a second interesting attribute – when used as part of a physical system authentication flow, this becomes a logical place to inject access control, being able to determine if some person is permitted to operate some device at that particular time (Is “Joe” current on his Laser Cutter certifications?) I think of the ideas I landed on, while conceptually interesting (using an employee id card as a 2FA token, it’s very fast), this one is the least likely to turn into something real.

The “encrypted-cookie”

This construction, when paired with an encrypted DESFire file, allows the “remote machine” to read/write an ’encrypted cookie’ to the card – storing small amount of encrypted data that the “remote machine” can read/write, but not the “reader machine”, since this uses an encrypted and authenticated channel from the “remote machine” directly to the DESFire card, without any intermediate hosts needing to be fully trusted. I keep calling this the “encrypted cookie” in my head because it feels conceptually similar to how Ruby on Rails and Laravel handles cookies.

We’d need to take a few extra steps here (for instance, ensure that you read the cookie back over the encrypted channel after writing to prevent a malicious reader from dropping writes) to secure the system, but it feels like the structure of this is definitely decent.

The “takeover”

This time, let’s say the computer attached to the NFC reader (“reader machine”) is semi-trusted. For this scheme, our trusted “remote machine” and the “reader machine” pass messages over the network to handle authentication to the card (as above), where the handshake data is being decrypted, encrypted and processed by the trusted “remote machine” as usual. However, once the authentication handshake is complete and a session key has been derived, the “remote system” return the session key to the “reader machine”, giving it a one-time-use key and authenticated session to the card.

We need to be careful about global/application permissions and key access control to files – but in this construction, we can allow the “reader machine” to take over privileged actions using a scope-limited DESFire key without handing over the card’s true keying material (preventing cloning of the card). This can be helpful to ensure messages to/from the card are truely from the card (verifying CMAC signatures), enables the “reader machine” to directly read/write to/from encrypted file(s), but allows the symmetric key material to remain in as few places as possible – which is critical given compromising that secret will undermine the security of the entire system.

  •  

Yves-Alexis Perez: IKEv1 protocol disabled in strongSwan package for Debian unstable

15 September 2026 om 09:28

Heads up, Debian IKE/IPsec users.

Starting with strongSwan 6.1.0-1 (currently in Debian unstable and targeted at Debian 14 Forky), the IKEv1 protocol has been disabled. This is aligned with upstream decision. Considering IKEv2 is already nearly old enough to drink in the USA (RFC 4306 will turn 21 next December) and IKEv1 has weaknesses, the disabling is long overdue amd will permit upstream to remove some code in the upcoming years.

At this point there is no good reason not to migrate to IKEv2 and exposing IKEv1 code in all Debian installation is no longer relevant. All IKEv1 users using Debian 13 Trixie (either site to site, gateway or roadwarrior client) should investigate IKEv2 protocol (or other options).

Note that some plugins have also been disabled upstream for security/maintenance reasons and we followed suite in Debian. The Debian relevant ones are: af-alg, led, padlock.

  •  

Freexian Collaborators: Debian Contributions: Security-tracker git performance, OpenSSH GSS-API split, Incus replacing lxc in Debian CI and more! (by Anupa Ann Joseph)

15 September 2026 om 02:00

Debian Contributions: 2026-08

Contributing to Debian is part of Freexian’s mission. This article covers the latest achievements of Freexian and their collaborators. All of this is made possible by organizations subscribing to our Long Term Support contracts and consulting services.

Replacing lxc with Incus for Debian CI, by Antonio Terceiro

At the beginning of this month, the default backend for tests was changed to incus-lxc, leaving only a few dozen packages explicitly configured to run as lxc. Those packages got bug reports filed and once those bugs are fixed they will be migrated over to incus-lxc. This marks an important milestone for Debian CI, as the low level of isolation between the worker host OS and the OS under test when using lxc was a significant source of instability in our platform. Common causes for test failures when comparing runs under plain lxc with runs with incus-lxc are documented in the wiki page linked from the bug reports.

security-tracker git performance, by Helmut Grohne

Developers working with Debian’s security-tracker have reported degrading performance for a while. The options for solving this are few with repository sizes now reaching 30GB for a repository whose working tree is a mere 60MB. While there have been a number of proposals for changing the storage layout, Helmut evaluated options not requiring such changes. Much of the problem hinges on the 50MB data/CVE/list file that is updated in most commits. Efficiency hinges on representing its content in git packs.

Practically, git frequently fails to represent its content as a delta and stores a full copy that is typically compressed to 12MB. Add a few thousand 12MB full blobs and you quickly have a large repository. In particular, the copy at salsa.debian.org stores many such full blobs, so pulling from it consumes significant bandwidth.

One might think that running git gc helps, but its utility is limited here. When git stores objects in packs, it stores the history in reverse. It starts with the current version and represents older objects as differences (deltas) to more recent objects. A delta effectively is a sequence of copying ranges from a base object and insertion of new content. Given that humans tend to add content over time, those additions are often represented as deletions. The crux here is finding that base object. Given that the data/CVE/list file is large, finding it involves comparing quite a few versions of it with each other. This requires both a significant amount of RAM and CPU time. How many objects git considers for this comparison is controlled with the --window option. It is beneficial if the base object does not come from a direct child commit but skips over commits. In doing so, long chains of deltas are avoided. The --depth option controls the maximum chain length.

In this search, git combines all objects into a single window to facilitate file renames. As such, it tends to compare data/CVE/list with data/DSA/list, data/DLA/list and others. This is less than helpful and tends to evict all data/CVE/list versions from the comparison window such that a new full blob of it becomes required. Since the security-tracker repository rarely renames files, we can ask git to instead consider one window per filename via --path-walk. Once doing so, it becomes quite a bit better at finding suitable deltas. The technique is less applicable to older history (2025 and earlier), but given a lot of RAM and a bit of partitioning, git gc can shrink that as well. Combining these techniques, we can shrink the repository into 700MB and keep new growth somewhat under control.

Temporarily, Helmut is providing a proof-of-concept mirror at git://git.subdivi.de/~helmut/security-tracker.git using these techniques. Avoid pulling from it directly as it does not provide a secure transport. While it does reduce the amount of data being downloaded, it does not resolve a number of related problems. After downloading, a git client will still expend considerable amounts of CPU on verifying received deltas and attempting to git blame data/CVE/list is not improved in any way. Still, there is work on integrating some of the improvements back into salsa.

OpenSSH GSS-API split, by Colin Watson

In an option review Colin did in 2024, shortly after the xz-utils backdoor, he explained that having GSS-API authentication and key exchange support in the main OpenSSH packages is problematic. The key exchange patch is large and intrusive. Even linking to the necessary libraries isn’t without risk: as the Ebury malware attack demonstrated way back in 2009, each extra library linked into security-critical daemons such as sshd (or nowadays into its privilege-separated helper programs) can modify the behaviour of the daemon even if you aren’t doing anything that would involve calling into that library. Of course some of that risk remains, but as Damien Miller wrote, minimizing the number of libraries that end up in the address space of sshd and friends is still valuable.

This split is now complete in testing. As of openssh 1:10.4p1-5, the OpenSSH client and server are built without GSS-API authentication and key exchange support. If you need those features, install openssh-client-gssapi or openssh-server-gssapi instead, as appropriate. Debian 13 (trixie) already has packages with those names that just depend on the regular openssh-client and openssh-server so that you can pre-emptively install them, as documented in the release notes.

The new openssh-*-gssapi packages have relatively tight dependencies on openssh-common, in order for the testing migration system to ensure that we can’t forget to keep them up to date. This will mean a bit more ongoing work on each new upstream version, but it should be manageable.

Catching up on DebConf video publication, by Stefano Rivera

DebConf 25 videos suffered terribly from a bug in voctomix that stopped us from being able to publish the videos immediately after the conference. Ivo De Decker did some work on the videos earlier this year, fixing enough of the audio problems to make the videos at least intelligible.

While at MiniDebConf Winterthur Stefano published all the videos from 2025 and 2026 to PeerTube and YouTube. DebConf 25 and DebConf 26 videos as well as all the videos from miniconfs that had got caught up in the backlog were published.

Miscellaneous contributions

  • Carles used po-debconf-manager to review and submit Catalan translations for six packages.
  • Carles prepared a prototype of building webwml and running “tidy” (linter) in Salsa.
  • Carles created different webwml merge requests with different fixes: invalid links to different architectures, 404s to external sites, fixed templates with invalid URL to favicon.ico, making USE_SAMPLE_FILES=1 to avoid fetching external files, typos in English, etc.
  • Carles updated the LibreOffice Wiki page.
  • Emilio managed several transitions, such as glibc 2.43, perl 5.42, kdepim, nginx, gnome-desktop 51, python3.14-only, and many others.
  • Emilio updated libxfont to 2.0.9, xorg-server to 21.1.24, xwayland to 24.1.13, and fixed a FTBFS bug in terminator.
  • Antonio deployed the website for MiniDebConf Beijing 2026, and archived the Hamburg 2026 and Kanpur 2026 MiniDebConf websites.
  • Antonio released auto-apt-proxy 18, adding new features and fixing bugs; that needed a followup 18.1 release to fix an issue identified in the testing migration tests.
  • Antonio did several Salsa maintenance tasks, including his first platform security update and investigating an issue that was causing the background tasks to be severely delayed, imparting the site usability.
  • Thorsten tried to fix some bugs in hplip and cups. Unfortunately this did not result in an upload this month, but stay tuned!
  • Helmut continued to report undeclared file conflicts and correspond about them.
  • Helmut reviewed and merged Johannes Schauer Marin Rodrigues’ work on adding hurd support to debvm.
  • Helmut fixed a pile of minor issues in the support libraries underpinning unschroot.
  • Helmut adapted rebootstrap to archive regressions and improved its handling of non-default gcc versions.
  • Helmut fixed the rendering of decompressed logs at crossqa.debian.net.
  • Helmut sent patches for two cross build failures.
  • Colin deployed the fix for Invalid link rel=“canonical” on bugs.debian.org. In the process he found a few bugs in recent undeployed code and fixed them.
  • Santiago continued helping Aryan Karamtoth with the GSoC project on introducing livepatching support for linux. Several challenges have been identified in the recent couple of months, and Santiago is trying to guide Aryan to address them and design the solution. The mentoring is done in collaboration with Emmanuel Arias.
  • Lucas onboarded a co-maintainer for Valkey.
  • Lucas has started to work on a draft for the Debian Outreach project for onboarding newcomers interested in Debian packaging via a video course.
  • Stefano worked on the after-math of DebConf 26, handling the bursary expense reimbursement process and winding down the conference finances.
  • Stefano uploaded Python 3.15 RC 1 to Debian unstable.
  • Stefano did some budgeting and travel planning for the DebConf 27 site-visit in Asahikawa, Japan. The DebConf committee sends a couple of members to meet with the local team and view the venue together, in advance of the next DebConf.
  •  

Reproducible Builds: Supporter spotlight: Jochen Sprickerhof on ... Reproducible Builds!

15 September 2026 om 05:54

The Reproducible Builds project relies on several projects, supporters and sponsors for financial support, but they are also valued as ambassadors who spread the word about our project and the work that we do.

This is the ninth installment in a series featuring the projects, companies and individuals who support the Reproducible Builds project. We started this series by featuring the Civil Infrastructure Platform project, and followed this up with a post about the Ford Foundation as well as recent ones about ARDC, the Google Open Source Security Team (GOSST), Bootstrappable Builds, the F-Droid project, David A. Wheeler, Simon Butler and Kees Cook.

Today, however, we will be talking with Jochen Sprickerhof, one of the newer members of the Reproducible Builds project core team.



Vagrant Cascadian: Could you tell me a bit about yourself? What sort of things do you work on?

Jochen Sprickerhof: I am a freelance programmer working on Open Source. Mainly doing Debian, F-Droid and some smaller software projects. In general I made it a habit to look into every software I use and try to fix bugs or add features I need. In Debian, I maintain about 180 packages with topics covering home banking, build systems and robotics. Most of my time, I currently work on reproduce.debian.net, where we try to bit-for-bit reproduce the packages distributed by Debian.


Vagrant: Could you describe the path that lead you to working on reproducible builds?

Jochen: I started my Debian journey as a teenager, converting my school to Debian and serving as its system administrator for 13 years. After studying Applied System Science, I joined the university’s robotics labs, where I worked on the Robot Operating System (ROS) and the Point Cloud Library (PCL). In the end, I enjoyed programming more than writing papers, so I eventually left academia for a robotics startup. Some years ago, I realized that the open source work I was doing in my spare time was actually the work I cared most about. Nowadays I am really grateful that I can spend my days working on things I find important and have lots of fun with.


Vagrant: What projects did you recently make big progress on?

Jochen: A recent example is metasnap.debian.net. It is a ‘meta archive’ of snapshot.debian.org which is itself archive of all packages in Debian. But let me explain it the other way round: with reproduce.debian.net, we try to reproduce the packages as they are distributed by the Debian archive. For that, we need the same build environment (compilers, libraries, build tools, etc) that was used by Debian back when the original package was compiled. Luckily, snapshot.debian.org has all those packages, but they are not easily accessible via apt, Debian’s package manager. So, metasnap provides a mapping from a package name and version pair to the APT repo on snapshot.debian.org needed to download it from. It was created by josch some time ago, and it’s awesome work. But when we tried to reproduce more and more packages on reproduce.debian.net, we found that some were missing packages from the build environment — even though they where visible on snapshot.debian.org. We found that metasnap excluded some archive areas because they where not expected to be needed. Reimporting all the data took more than two months and surfaced a couple more flaws.

With this fixed, we were able to build more packages, only to find out that metasnap also needs better support for version numbers. Luckily we were able to rewrite the data in a day instead of starting the import again.


Vagrant: You have been working on infrastructure to support reproducible builds for a while. Has recent adoption of reproduce.debian.net into the Debian release tooling changed the focus of your work?

Jochen: Quite a bit. When we started reproduce.debian.net in 2024, only around 33% of the packages could be reproduced successfully. Today we are above 98%. Most were not bugs in the packages themselves but in the infrastructure. Similar to the metasnap issue I reference above, packages just needed a rebuild because something else, like the toolchain, was fixed in the meantime. In May, people from the Debian release team and the Reproducible Builds project sat together and decided that the overall state is good enough, and now packages that regress on reproducibility are blocked from entering the next Debian release. But that does not mean all the work is on the shoulders of Debian package maintainers. Since then I have been constantly looking at the migration tooling to spot regressions and provide fixes. Furthermore, a couple of maintainers reached out to us for help and I hope more will do so in future.


Vagrant: What is one small thing you (or others) have not yet gotten to that you would really like to see?

Jochen: The central tool to reproduce Debian packages is debrebuild, also written by josch. Currently it has two ways to retrieve the build dependencies of a package. Either it uses metasnap.debian.net (as explained above), or it can access the Debian unstable APT repository directly. This allows to test packages locally before everything is indexed on metasnap by compiling against Debian unstable. But actually there are many other APT repositories to query, like Debian stable or even derivatives. Adding support for an optional list of APT repositories in debrebuild would be great. That would also be a big step to support reproducing other Debian based distributions.


Vagrant: … and one big thing?

Jochen: It would be great to integrate metasnap.debian.net into snapshot.debian.org. There is some discussion on it already in Debian bug #650783.


Vagrant: What are the tools you use the most?

Jochen: According to my fish shell history:

$ history | cut -d' ' -f1 | sort | uniq -c | sort -nr | head -10
  36199 git
  20941 vi
  12271 rm
   8599 cd
   7917 ls
   6407 apt
   5631 grep
   4249 mv
   3655 dpkg
   2873 cp


Vagrant: So, is the fish shell reproducible? I remember it did not used to be…

Jochen: You can check for yourself — it was last time I checked. But looking through the other commands, neovim sadly is not. I hope we can fix that in future.


Vagrant: Oh, that’s a nice URL to check for reproducible package… you can just pass the source package name to check the current results?

Jochen: Yes. Another one is udd.debian.org/reproducibility/, where you can list all packages of a Debian maintainer. It also lists source reproducibility and has nice filters as well.


Vagrant: What tools do you use specifically working on reproducible builds?

Jochen: I don’t have statistics for that, but I would say sbuild to build the package, debrebuild to reproduce it, and diffoscope to analyze the differences. Obviously I also need run apt source <package> or use git-buildpackage to get the sources and all the tools I mentioned above.


Vagrant: So how many packages are left to build reproducibly, and once those are finished, what is next?

Jochen: Right now, reproduce.debian.net shows over 98% reproducibility, though there are still over 650 package left and some will probably need a lot of work. But actually I think making packages reproducible is just the first step. For me, this is a project to build confidence in the system. To reproduce a package we have two parts: the source of the package and the build environment. Fixing the packages means gaining confidence in the first part but we still rely on the individual build environments for each package as we need to use the same compiler that was used when the package was build initially. Because of this, we have to keep around every historical version of all toolchain packages. I really would like to remove this extra archive, which means we would have to rebuild all of Debian around release time. I am dreaming of a Debian release where you could bit-for-bit reproduce every package just from the released versions. Due to how Debian works, however, this is not a trivial rebuild and it would need some work on the infrastructure. By the way, initially there was a third component to pay attention to: any connection to the outside world during the build. Luckily we fixed the Debian build daemons to not allow network connections during the build some time ago.


Vagrant: Thanks for all that work, and taking the time to tell us a bit about yourself!

Jochen: Thanks a lot for the interview!




For more information about the Reproducible Builds project, please see our website at reproducible-builds.org. If you are interested in ensuring the ongoing security of the software that underpins our civilisation and wish to sponsor the Reproducible Builds project, please reach out to the project by emailing contact@reproducible-builds.org.

  •  

Enrico Zini: Financial risks in 2026

8 September 2026 om 09:05

I asked the banker who is my reference at the bank something like this:

Give that we are talking about the consequences of the tantrum of a fascist foreign government, what happened to them (who are also people close and dear to me), in some future can very well happen to me.

Suddenly my risk profile shot up under the roof.

What do you suggest me to do? Should I find a trusted source of gold bullions to bury under the cellar at home?

The answer was something like this:

Sadly YES, given that the USA have a sort of financial monopoly they can entitle themselves to arbitrarily define a person/organization as a terrorist without any trial or judicial course, and as a consequence apply sanctions that cannot be effectively counteracted, not even abroad.

I didn't have this in my 2026 bingo card, but here we are.


For more details, see:

For some broader context on this kind of actions from the USA, see also:

  •  
❌