.__                         .___               
_______   ____    ____________  |__|  ____    ______      __| _/  ____  ___  __
\_  __ \ /  _ \  /  ___/\____ \ |  |_/ __ \  /  ___/     / __ | _/ __ \ \  \/ /
 |  | \/(  <_> ) \___ \ |  |_> >|  |\  ___/  \___ \     / /_/ | \  ___/  \   / 
 |__|    \____/ /____  >|   __/ |__| \___  >/____  > /\ \____ |  \___  >  \_/  
                     \/ |__|             \/      \/  \/      \/      \/        
        

root@romancentral~ $ cd /opt/projects && docker compose up -d

Anabasis C++23 Released · v1.0

A compliance scanner for Linux host firewalls. Whether a firewall enforces what somebody decided is a question about sets of packets rather than about individual rules, and the research that treats it as such - FIREMAN, Margrave, the Isabelle/HOL semantics out of TU München - was written for iptables and Cisco ACLs, while nftables quietly became the default underneath Debian and RHEL. The commercial tools (Tufin, FireMon, AlgoSec, Batfish) concern themselves with appliances; a Linux host, with Docker, libvirt and CrowdSec each writing into the same ruleset, is precisely the case none of them was built for. Anabasis reads the ruleset the kernel has actually loaded, derives from it the set of packets that will be accepted, and holds that set against a declarative specification kept under version control. The reasoning behind all of it, from the packet-space algebra to the kernel oracle, is set out at length in an eight-part series on the blog; what follows is the abridgement.

  • Packets as points, policies as shapes: an in-house reduced ordered BDD kernel, after Bryant, renders a packet space of more than 150 boolean dimensions exactly queryable, so that “can anything outside the admin network reach port 22?” receives an answer, a deterministic witness packet and the size of the hole - whatever the rule order, shadowing or phrasing
  • Nothing is guessed. A rule that cannot be decided statically - a rate limit, an osf match, a hand-off to user space - is carried as a bracket, under ⊆ real ⊆ over, and the finding it touches is reported inconclusive, naming rule and reason, for a pass the tool earned and a pass it invented would otherwise read identically
  • The semantics are those of nftables itself rather than a borrowed iptables abstraction: several base chains per hook, where accept is local and drop is final; jump, goto and return; verdict maps and sets; and NAT, modelled as the preimage of its rewrite, so that every question concerns the packet that arrived rather than the one the filter ends up seeing
  • Beneath the ruleset lies the host, which decides whether a packet reaches a chain at all - routing, forwarding switches, reverse-path filtering as the kernel combines it, bridges with and without br_netfilter. Whatever was not read is stated in the report as an assumption rather than silently presumed
  • The tool is examined against a real kernel, not merely against itself: each oracle test forks into its own unprivileged user and network namespace, loads the ruleset through libnftnl, sends real packets across veth pairs and compares the kernel’s own trace with both engines, step by step rather than verdict by verdict
  • Intent is written in anabasis/v1, a specification language that speaks of flows rather than rules, with address and service books, inheritance through extends, and a canonical form that keeps review diffs meaningful (spec fmt --check enforces it in CI). The surface is frozen and admits additions only
  • A finding may be accepted, though only by its full fingerprint, with a reason and an expiry date, so that accepting one violation never quietly accepts the next; and since the same policy dumped by nft 1.0.9 and by 1.1.7 yields byte-identical fingerprints, such acceptances outlive an nft upgrade
  • A lint layer after FIREMAN, extended by the anomalies peculiar to nftables - an accept undone by a later base chain, chains nothing reaches, sets nothing consults. It is useful on the first morning with an unknown host, and no substitute for a specification thereafter
  • Every finding arrives with a witness packet, the violating region as ranges over header fields and the rule that decided it; output as text, JSON or SARIF 2.1.0, the last anchored to the line of the specification a reviewer actually edits
  • Performance was settled by measurement rather than by expectation: a complete check of a production ruleset carrying a 23,621-address blocklist takes 67 to 82 ms, and every design decision is recorded with its numbers - the wrong turns included
  • Each commit is built by GCC and Clang and put through clang-tidy, PVS-Studio, SonarQube and the kernel oracle; benchmark regressions are gated by Bencher and reported into the merge request by bencher2gitlab
C++23 nftables libnftables libnftnl netlink BDD packet-space analysis three-valued logic network namespaces compliance SARIF Conan2 Bencher
Acheron C++23 Active development

Acheron fills the evident gap in dependency scanning for C++ ecosystems. Born after one too many attempts to bend general-purpose scanners to a C++-shaped world - tools like trivy or grype are formidable in their own domains, but none felt at home with recipe-only remotes and source-built libraries. Acheron keeps the lockfile as the single source of truth: a sober ledger of what enters the build, independent of how artifacts are produced. The Conan 2 path is implemented end to end today; Bazel and vcpkg are architected behind the same plugin layer, scheduled as phases 2 and 3.

  • End-to-end Conan 2 pipeline: zero-copy conan.lock parsing, joined against the local Conan cache database for recipe and source paths
  • Four-stage provenance waterfall - user overrides → OSV determineversion source-file fingerprinting (parallel MD5 hashing of the package's sources) → conandata.yml URL/tag extraction → an honest “could not assess” surfaced in the report diagnostics instead of a silent gap
  • Batch vulnerability assessment against OSV.dev over a C++20-coroutine HTTPS client (Boost.Asio/Beast): connection pooling, exponential-backoff retries, and a lock-free three-state circuit breaker
  • Persistent RocksDB TTL cache for provenance lookups - zero-copy batched reads, bloom filters, atomic write batches
  • In-house CVSS calculator covering v3.0, v3.1 and v4.0 (base, temporal, environmental - spec-accurate rounding included), plus EPSS score enrichment via FIRST.org
  • Three report formats: SARIF 2.1.0, GitLab Dependency Scanning 15.2.3, and CycloneDX 1.7 VDR - provider query failures travel into the reports as scan messages
  • Architecture built on five C++20 concepts with a type-erased engine core; ecosystems plug in as compile-time traits over a variant - the seam Bazel and vcpkg will slot into
  • Uniformly std::expected-based error handling (no exceptions in the domain layer) and full OpenTelemetry instrumentation: nested spans and metrics from cache hits to circuit-breaker trips
C++23 coroutines Boost.Asio/Beast Conan2 Bazel vcpkg SBOM RocksDB OSV.dev CVSS EPSS SARIF GitLab SCA CycloneDX VDR OpenTelemetry
bencher2gitlab Go Released

A benchmark verdict is worth as much as the attention it receives, and in a merge-request workflow attention is spent in the merge request. Bencher already decides on the server whether a change has regressed and posts that verdict into GitHub pull requests; for GitLab its documentation stops at an invitation to call the API oneself. bencher2gitlab is that call. It takes the report bencher run produces, sets it into the merge request as a table, keeps it current across pushes and, where the project asks for it, withholds the merge until somebody has signed a regression off. It computes no statistics of its own - the verdict is Bencher’s, and the tool’s whole ambition is that it be read.

  • One comment per context, however often the branch is pushed. Each carries a hidden marker keyed on project, branch, testbed and adapter, so that a later push rewrites the comment it finds rather than stacking another beneath it, and parallel testbed jobs - gcc and clang, say - keep a comment each without knowing of one another. Only comments written by its own token user are ever edited, so a reviewer who happens to quote the marker cannot send it off to rewrite somebody else’s note
  • A regression can be made to block the merge rather than merely be reported. With --thread, and GitLab’s “all threads must be resolved” in force, the report opens a resolvable thread, and a sign-off, once given, holds: the thread keeps a hidden list of fingerprints - benchmark, measure, boundary - of the regressions that were on the table when it was resolved, and later pushes leave it alone until a print appears that nobody has yet seen. Once every alert is gone it resolves itself; a first run that is already green creates it resolved
  • Post first, fail second. Left to bencher run --err, a regression ends the job before any report exists, and the one piece of information the merge request needed is left in a log nobody opens. bencher2gitlab posts the comment and only then exits non-zero, so that the job turns red with its reason already in place
  • Three exit codes, and a precedence among them: 0 for no alerts, 1 for alerts once the comment is posted, 2 for an operational failure - which outranks 1, so that a verdict that could not be delivered is never mistaken for one that was
  • Retries follow what a request may safely repeat. Backoff is exponential and honours Retry-After, abandoning the attempt at once when the server names a moment beyond the deadline rather than sleeping toward a certain failure. Reads and edits retry on 5xx; creating a comment retries only on 429. POST is not idempotent, and RFC 9110 advises against retrying it unless the client can tell the first attempt was never applied: after a 5xx or a dropped connection GitLab may well have stored the note, whereas the rate limiter turns a request away before anything is written
  • Credentials are read from the environment and never from flags, so nothing surfaces in ps output or shell history. What GitLab will not allow is documented rather than left to be discovered: CI_JOB_TOKEN may read merge-request notes but meets a 403 on writing one, so a project access token is required
  • Plain Go, without a single dependency; a handful of REST endpoints did not seem to justify an SDK. The report types were written against Bencher’s own Rust definitions and its OpenAPI specification, and are parsed leniently by design, so that a field added upstream is ignored rather than fatal
  • No test ever reaches a real GitLab. An httptest fake of the Notes and Discussions APIs covers pagination, thread resolution, comments by other users, injected 429 and 5xx responses and the job token’s 403, and the Markdown rendering is pinned by golden files
Go Bencher GitLab continuous benchmarking CI/CD merge requests zero dependencies SonarQube
BankingSync Go Released · v4

A self-hosted bridge that carries bank transactions from Enable Banking (PSD2 / Open Banking) into Actual Budget or Firefly III, on a read-only consent and without a third party ever holding your credentials. Fetching the transactions is the easy part. Whether two lines on a statement are one payment is the hard one: the bank reports a purchase once as an authorisation and again as a booking, under a different payee, for a different amount and - since many banks issue the entry reference only at booking - often with nothing to tie the two together. Official statistics settled that question decades ago as record linkage, and BankingSync borrows the answer rather than inventing its own. What did not survive holding the implementation against that literature is set out in the v4 release notes on the blog; what follows is the abridgement.

  • One interface, two budgets: Actual Budget and Firefly III sit behind the same abstraction, chosen by a single environment variable, and every scenario in which they could disagree runs against both, so that a difference in behaviour surfaces in the test suite rather than in somebody’s budget
  • Matching is record linkage, not a rule set - the part of this project I got wrong first. A meal authorised as “Da Luigi Roma” returns as “Visa Da Luigi”, the card scheme prefixed and the tail cut to fit the field, and string similarity cannot tell that from a different branch of the same chain: “Shell Tankstelle” against “VISA Shell” and “Da Luigi Roma” against “Visa Da Luigi Milano” both score 0.667 under Sørensen-Dice, and Jaro-Winkler fares no better. Truncation removes a word; a different branch contradicts one. The Fellegi-Sunter model (1969) draws exactly that distinction - each field yields a discrete comparison level worth log₂(m/u) bits of evidence, and the bits sum to a probability
  • A statement is decided as a whole rather than pair by pair. Linkage becomes an assignment problem, as Jaro read it in 1989, solved with the Hungarian method; weighed one at a time, two bookings could lay claim to the same authorisation, the order of the loop broke the tie, and the same statement read in a different order produced a different budget
  • Where the model is unsure, it says so. Two thresholds leave a band between them - the clerical review zone the model has always had - and whatever lands there waits for a person instead of being guessed at; an answer given against a page that has since gone stale is refused rather than applied
  • The parameters are argued for rather than asserted. A generated table moves each of them by a factor of three and counts the decisions that change, and the build refuses any change to it that nobody has read. It learns from your answers, from pairs the bank’s own reference confirms and, if you allow it, from one question per sync; the non-match half needs no answers at all, since at most one candidate in a window can be the counterpart. Nothing it learns takes effect on its own: a refit runs in shadow first, then passes a gate - six behaviours unchanged, a Brier score better than chance can explain on evidence the fit never saw, and a person pressing the button
  • The opening balance is written once and never revised. Whatever predates the first import is recorded and from then on only compared, so that drift is reported rather than quietly smoothed over
  • A renewed consent is a new session, and a new session means new account identifiers - Enable Banking’s are valid only for as long as the session that issued them. The account is carried across by its identification hash, then by IBAN and currency, then by the only account on offer, and where doubt remains, you choose rather than the software. A session the bank has closed is marked as awaiting renewal rather than mistaken for a refused balance scope, which is how, until recently, a single closed session managed to switch off balance checks for good
  • Tested against a real Firefly III rather than a mock of one: an instance is brought up and torn down for every merge request, deliberately in a timezone west of UTC - which is what exposed a date bug the hand-written fixture had been agreeing with all along
  • Whatever the matcher decides can be traced after the fact. Metrics, traces and logs leave over OpenTelemetry, profiles for Pyroscope; the distribution of match probabilities is recorded because without it the thresholds are guesswork, and every decision the model makes carries a span that breaks its weight down term by term, so that a merge can be explained a month later rather than merely stated. A test holds the telemetry reference against the source, because an alert on a series that has quietly ceased to exist does not fail - it simply never fires
  • The web interface exists for the decisions that are yours to make - the review queue, and the parameters your answers would support beside the ones in force - and warns by email before a consent lapses. It speaks TLS with a self-signed certificate out of the box, or plain HTTP behind a reverse proxy that already terminates it, in which case forwarded headers are honoured only once you have declared that a proxy you control sits in front of it
  • One static binary in a non-root image, for amd64 and arm64 alike, so a Raspberry Pi with a 64-bit OS will serve. The ARM image is cross-compiled rather than emulated, which asks nothing of the build host, and every image ships a CycloneDX SBOM with BuildKit attestations on Docker Hub. Both the ARM build and the plain-HTTP option began life as pull requests from outside
Go SQLite PSD2 Open Banking Actual Budget Firefly III OpenTelemetry Pyroscope CycloneDX Docker arm64 record linkage self-hosted
nowplaying Rust Live in production

Knowing what is playing is trivial for the device that plays it and surprisingly awkward for anybody else. nowplaying is the service behind the live music widget on the home page, written to retire Last.fm and its third-party widget altogether. Rather than wait to be told, it joins the account as a Spotify Connect device in its own right, through librespot, and reads the cluster updates Connect distributes to every device - so that it sees whatever plays anywhere on the account, not only what it happens to be playing itself. Each play reaches the visitor over a WebSocket the moment it begins.

  • The page shows the player’s actual state rather than a guess at it. Pause, resume, seek and stop travel as light state frames, each carrying a position and the server time at which it held, and the browser extrapolates only while the track is playing - a paused track stays paused on the page instead of running on until the next one corrects it. Not every update is news: a seek is published only beyond three seconds while playing, or half a second while paused, and a repeated device update never twice
  • A play counts only after thirty seconds of listening, so that skipping through a queue leaves no trace in the statistics - a simpler threshold than Last.fm’s own, which asks for half the track or four minutes, whichever comes first. The top songs and artists of the last thirty days are read from Postgres; podcast episodes are recognised for what they are, linked to their own page and kept out of both
  • A private session stays private. While the playing device reports one, nothing about what it plays is looked at, published or stored - the page merely sees the last public track come to a stop, and the play that was under way is dropped rather than counted
  • A crash costs nothing and counts nothing twice: a replay guard and idempotent inserts see to it that a kill -9 followed by a restart never records the same play again. Lesser mishaps cost no more: a play under way outlasts a dropped connection to Spotify with the listening already done kept to its credit, and an insert that Postgres refuses is tried again, at growing intervals, before the play is given up
  • The service answers to this domain and to no other. A WebSocket handshake from a foreign origin is refused before the upgrade, CORS admits cross-origin reads from this domain alone, and TLS ends at nginx, behind which the application listens on loopback and nowhere else
  • Instrumented as thoroughly as anything on this page. Traces, metrics and logs leave over OTLP, metric cardinality is strictly bounded, and a single cluster update can be followed from the moment it arrives to the play it eventually persists; the uneventful ones are thinned out downstream, by the tail sampling of the stack below
  • The widget at the other end is plain JavaScript without a dependency. It takes its colours from the album cover as each track begins and lays the lyrics behind the title as a washed-out backdrop, synchronised line by line - scrambled on the server beforehand, so that the words themselves never leave it
Rust librespot axum WebSockets Spotify Connect PostgreSQL sqlx OpenTelemetry nginx self-hosted
Observability Stack Ansible / IaC Live in production

A monitoring system is only as trustworthy as the least trustworthy thing permitted to write into it, and it fails in the one manner that is hard to notice - by falling quiet. This one is a self-hosted, multi-tenant platform for metrics, logs, traces and continuous profiling: Mimir, Loki, Tempo and Pyroscope over self-hosted S3 on MinIO, with Grafana and HAProxy in front and Alertmanager paging through PagerDuty, provisioned in its entirety from a single Ansible monorepo. What it watches has grown into a small fleet, each host a tenant of its own: the web frontend with its nginx and the nowplaying service, the Go blog engine, a grommunio mail server, two CI build hosts, the banking bridge above, and the observability host itself. The traces whose fourteen-day retention the legal page discloses are kept here.

  • Tenancy is a matter of cryptography rather than of courtesy. step-ca runs in Registration Authority mode, its signing key confined to a network segment nothing outside can reach; every host holds a SPIFFE-style certificate valid for twenty-four hours, and HAProxy derives the tenant from that certificate’s CN at the edge, setting it outright rather than trusting whatever the client might claim
  • Certificates renew every fifteen minutes through systemd template units and are taken up by the collector while it runs, without a restart and without a gap in the telemetry; should the CA itself be rotated, the new root reaches every host of its own accord
  • More than eighty alert rules in seven groups, written with an eye to how alerting fails. A dead man’s switch fires permanently, so that its silence is what pages. 4xx alerts come in pairs: a ratio, held above a floor of traffic so that three 404s out of five requests at night wake nobody, and an absolute count for the flood during which total traffic rises with the attack and the ratio stays unremarkable
  • Rules are layered by tenant. A shared baseline applies to every host; where a service has earned a set of its own - the frontend, with its nginx, Postgres and ActivityPub federation - an override replaces that baseline wholesale rather than amending it; and a tenant that is only a container, the banking bridge, which has no host to speak of, ships its own file and nothing else
  • Every exporter is held to an allow-list at the scrape, so that a talkative binary cannot widen the series set unannounced. What passes is chosen: nginx traffic by country and by legitimate against blocked, the ban rates of CrowdSec and fail2ban, Postfix deliverability down to the DSN of each deferral
  • Exporters are installed as the host deserves rather than as upstream suggests: sandboxed systemd units, dedicated users, sudoers whitelists that visudo has validated, pinned virtual environments - and beside them an encryption-health checker of its own for LUKS and the TPM, reporting through the textfile collector
  • Traces are sampled at the tail, because the attribute that separates noise from signal exists only once a span has finished and cannot be consulted at its head: uneventful outcomes are kept at ten per cent, everything else in full
  • Three playbooks cover the whole life of the thing - provisioning, idempotent reconfiguration, onboarding a host - with restart handlers bound file by file, so that drift in one configuration restarts one service and no other. The observability host is onboarded as a tenant like any other, and so watches itself by construction
  • The kernel, the NVMe devices and Docker are tuned, and every value carries its reason: conntrack timeouts set against gRPC’s keepalive so that the two cannot race, BBR, dirty_bytes in absolute bytes, since a percentage of a large memory is no limit at all. A self-test asserts each value after every reboot
  • Secrets are kept in two tiers, ansible-vault and GPG-backed pass, both encrypted at rest in git. Backups leave the site with restic and respect how each store may be read: what cannot bear a concurrent reader is stopped first, and where downtime would buy nothing, a live Postgres dump is streamed instead
Ansible Grafana Mimir Loki Tempo Pyroscope Alloy MinIO step-ca mTLS HAProxy PagerDuty CrowdSec restic