sakana

This manual documents sakana version 0.1.0, a lightweight monitor for GNU Taler exchanges.

Copyright © 2026 Taler Systems SA.

Permission is granted to copy, distribute and/or modify this document under the terms of the GNU Affero General Public License, version 3 or any later version published by the Free Software Foundation.

Table of Contents


1 Introduction

sakana periodically asks every configured GNU Taler exchange for its /keys endpoint and reports anything that looks wrong: an unreachable host, a refused port, a broken TLS handshake, a TLS certificate that should have been renewed, an unexpected HTTP status, a timeout, a /keys response that does not parse, key material that has expired or is about to expire, and fees that are not configured for the period they must cover.

Reports are not sent anywhere by sakana itself. Instead, sakana forks and executes a command from its configuration and writes the report to the standard input of that command. Whether a report becomes a mail, a page, a line in a log or an entry in an incident tracker is therefore entirely up to the operator.

1.1 Design decisions worth knowing

No state on disk

Failure counters live in memory. A restart of sakana starts every counter from zero, which means the first check after a restart reports an existing problem immediately.

One counter per error class

Every error class of every exchange counts its failures separately. A denomination warning that has been pending for weeks can therefore not delay the report of an exchange that has just become unreachable.

Unevaluated classes keep their counters

While an exchange cannot be reached, nothing can be said about its keys. The key classes are then neither escalated nor reset, so a pending warning survives an outage and does not produce a second alert when the exchange comes back.

Expired keys are normal

An exchange keeps publishing keys whose validity has ended. sakana reports expiry only when no usable key remains, which is the situation in which key rotation has actually stopped. Signing keys and denominations have separate warning horizons, since exchanges provision them months and days ahead respectively.

No dependencies

sakana is written in Go and uses only the standard library, including for TLS, HTTP and JSON.


2 Installation

sakana needs Go 1.22 or newer and nothing else. The optional makeinfo program from texinfo renders this manual as info and html.

./bootstrap
./configure --prefix=/usr/local
make
make check
sudo make install

./bootstrap checks the toolchain and installs a gofmt pre-commit hook. ./configure is a hand-written script in the style used throughout the GNU Taler code base; it accepts the usual directory variables (--prefix, --bindir, --sysconfdir, --mandir, …) and tool overrides such as GO=/opt/go/bin/go. Run ./configure --help for the list.

make install installs

bindir/sakana

the binary,

sysconfdir/sakana/sakana.conf

the configuration, from the example file, if it does not exist yet,

mandir/man1/sakana.1
mandir/man5/sakana.conf.5

the manual pages,

prefix/lib/systemd/system/sakana.service

the systemd unit,

datadir/sakana/sakana-notify-mail

an example reporting command, and

docdir

this manual, the example configuration and the license.

make check runs the whole test suite. It needs neither network access nor an installation. make check-live additionally checks the public exchanges and does need network access. make help lists all targets.


3 Configuration

The configuration is an INI file. The manual page sakana.conf(5) documents every option in reference form; this chapter explains how the options fit together. A minimal configuration looks like this:

[sakana]
FREQUENCY = 15 m
TOLERANCE = 3 10 300
COMMAND = /usr/share/sakana/sakana-notify-mail ops@example.com

[exchange-taler-ops]
BASE_URL = https://exchange.taler-ops.ch/

The optional [sakana] section holds defaults. Every other section describes one exchange, and its name — exchange-taler-ops here — appears in every report and log line about that exchange. Each exchange needs four values, either in its own section or as a default: BASE_URL, FREQUENCY, COMMAND and TOLERANCE.

3.1 Durations

Durations use the Taler and GNUnet spelling: 5 s, 10m, 1 h, 1 d, 4d 32s. The units are us, ms, s, m, h, d, w and y, with the long forms (seconds, minutes, …) and the abbreviations " and ' accepted as well.

3.2 Frequency and jitter

The delay between two checks of one exchange is FREQUENCY randomized by ±5%. Several monitors watching the same exchange therefore drift apart instead of hitting it in lockstep. The first check of every exchange happens immediately when sakana starts.

3.3 Tuning the horizons

TLS_RENEWAL_HORIZON (five days by default) is the point at which a TLS certificate that has not been renewed becomes a report. Five days is a deliberate compromise: certbot renews at 30 days remaining and retries twice a day, so a certificate with less than five days left means that renewal has been failing for weeks.

The key material has two horizons, because exchanges provision its two kinds on entirely different scales.

SIGNKEY_EXPIRATION_HORIZON (30 days by default) is the point at which expiring online signing keys become a report. Exchanges sign those months ahead—the two public exchanges are currently at two months and ten months respectively—so 30 days of notice is both ample and easy to satisfy.

DENOMINATION_EXPIRATION_HORIZON (14 days by default) is the point at which denominations that can no longer be withdrawn become a report. It is shorter on purpose: denominations are rotated within days, and an exchange publishes them only as far ahead as its LOOKAHEAD_SIGN reaches. The demo exchange at https://exchange.demo.taler.net/ publishes them about 30 days ahead and dips to roughly three weeks between signing runs, so a 30 day horizon would warn about an exchange that rotates exactly as intended. A horizon below the lookahead of the deployment still catches the real failure, because a signing pipeline that has stopped lets the remaining horizon shrink to zero.

Raise the denomination horizon if the exchange you monitor signs further ahead and you want earlier warning; KEY_EXPIRATION_HORIZON sets both at once if you want one knob.


4 What sakana checks

Every check is one GET request to BASE_URLkeys. Along the way, sakana evaluates each of its error classes and arrives at one of three verdicts for it: ok, fail or unknown, the last meaning that this check could not say anything about the class.

4.1 Reaching the exchange

Name resolution, the TCP connection and the TLS handshake are performed by sakana itself rather than left to the HTTP client, so that each failure can be classified precisely:

dns-failure

The host name does not resolve. The report distinguishes a name that does not exist from a resolver that did not answer.

host-unreachable

The connection attempt was answered with neither a SYN-ACK nor a RST, or an ICMP host or network unreachable arrived.

port-unreachable

The connection was refused with a RST: something is listening on the host, but not on this port.

4.2 The TLS connection

sakana validates the certificate chain itself, with the system trust store plus every root given with --trust. Because validation happens after the handshake, with the chain in hand, the report can name the certificate that is at fault even when it is rejected:

tls-handshake

The handshake failed, or validation rejected the chain. The detail names the reason: a certificate that is not valid at this time, an unknown certification authority, a host name mismatch, a peer that did not speak TLS at all, or the TLS alert the peer sent.

tls-expiring

The handshake succeeded, but the certificate expires within TLS_RENEWAL_HORIZON.

4.3 The HTTP response

http-status

The status was not 200 OK. The report includes the Taler error code and hint if the body carries them, and adds what a 502 or a 503 from /keys usually means.

http-timeout

The connection was established, but no complete response arrived within TIMEOUT.

keys-parse

The body is not a JSON object, or not an exchange /keys object: a missing version, currency, master_public_key, signkeys, denominations, global_fees or wire_fees member, or a malformed timestamp, all land here.

4.4 Key material

Both key checks ask whether anything usable is left. Keys whose validity has ended stay in the response and are not by themselves a problem.

signkeys-expired

No entry of signkeys has a stamp_expire in the future. The exchange cannot sign anything and is effectively out of service.

signkeys-expiring

No entry of signkeys is valid beyond SIGNKEY_EXPIRATION_HORIZON.

denoms-expired

No denomination has a stamp_expire_withdraw in the future, so no coin can be withdrawn any more. Denominations marked lost are not counted as usable.

denoms-expiring

No denomination can be withdrawn beyond DENOMINATION_EXPIRATION_HORIZON.

When the expired class fires, the expiring class of the same kind is left unknown instead of being reported as well: the worse report subsumes the milder one, and the milder counter neither escalates nor resets.

4.5 Fees

Fees must cover the period from now until the end of the current year, and, during December, until the end of the next year as well — by December an exchange has to have the fees of the coming year signed. sakana sorts the fee periods, walks them and reports the first moment that is not covered. Periods that end exactly when the next one starts are contiguous, which is how consecutive years are configured.

global-fees-missing

global_fees does not cover the required period.

wire-fees-missing

The wire fees of at least one wire method in wire_fees do not cover it. The report names the methods and their configured periods.


5 Failure tolerance and service restoration

TOLERANCE is a sequence of counts. With 3 10 300:

  1. failures one, two and three of a class are tolerated silently;
  2. the fourth is reported;
  3. ten further failures are tolerated, and the eleventh after the report — the fifteenth in total — is reported;
  4. 300 further failures are tolerated and the 301st is reported;
  5. the last phase repeats forever, so with a frequency of 15 minutes a persistent problem produces a reminder about every three days.

Any check in which the class is ok resets the sequence to its beginning, so the next problem is again reported at the fourth consecutive failure. A check in which the class is unknown changes nothing.

Warnings use the same machinery as outages. A certificate that certbot failed to renew is therefore reported once, then on an increasingly sparse schedule, rather than on every single check.

When a check finds every class in order — the response was fetched and parsed and nothing failed — and at least one report had been made since the last restoration, sakana delivers one brief service restored report naming the classes that had been reported, and forgets them. Failures that were tolerated and never reported produce no restoration report, because nobody was told about them in the first place.


6 Reporting

COMMAND is split into an argument vector the way a shell would split it — single quotes, double quotes and backslashes work — and is then executed directly. There is no shell, so a redirection in COMMAND does nothing; write /bin/sh -c '…' if a shell is wanted.

The report is written to the standard input of the command. A command that does not read its input is not an error: sakana ignores the resulting broken pipe. The command runs in its own process group and is killed, with its children, if it exceeds COMMAND_TIMEOUT.

6.1 The report

The first line is a one-line summary suitable as a mail subject:

sakana ALERT https://exchange.example.com/: TLS certificate expiring

A blank line follows, then the headers:

Exchange:     https://exchange.example.com/
Endpoint:     https://exchange.example.com/keys
Host:         exchange.example.com
Error class:  tls-expiring
Severity:     warning
Detected:     2026-11-03T21:30:00Z
Occurrences:  4 consecutive failures of this class; 3 tolerated before this report
Monitor:      sakana 0.1.0 on monitor.example.com, configuration section [exchange-demo]

Then the detail of the class — for a certificate that is expiring, its subject, issuer, names and validity, and what to check about certbot — and finally a footer that says after how many further checks the class would be reported again.

The restoration report follows the same shape, with the summary

sakana RESTORED https://exchange.example.com/: service restored

6.2 The log

For every report sakana writes one line to standard error:

2026-11-03T21:30:00Z ERROR   ERROR https://exchange.example.com/ unexpected  HTTP status [http-status] -- command ok after 7ms, output: "accepted"

It carries the severity, the exchange URL, the error class in two or three words, the class identifier and the result of the reporting command, including its exit status and its output. Output of the command is collapsed to one line and truncated, so that one event stays one line.


7 Running sakana

sakana [-c FILE] [-R FILE]... [-L LEVEL] [-1] [-t] [-v] [-h]

The options are documented in the manual page sakana(1). The two that matter most in daily use are --test, which prints the schedule that results from a configuration without contacting anything, and --once, which checks every exchange once, reports as usual and exits non-zero if anything was wrong.

7.1 Under systemd

The shipped unit runs sakana as the unprivileged user sakana with a tight sandbox and logs to the journal:

systemctl enable --now sakana.service
journalctl -u sakana -f

Note that the sandbox applies to the reporting command as well, since it is a child of sakana. A command that needs more than outgoing network access and a read-only file system needs the unit relaxed accordingly, which is why the unit is a plain file in /usr/lib/systemd/system and meant to be overridden with a drop-in.

7.2 From cron

A one-off check fits a cron job:

*/15 * * * * sakana --once --config /etc/sakana/sakana.conf

Keep in mind that each invocation starts with empty counters, so a tolerance sequence cannot build up across invocations; use TOLERANCE = 0 in this mode, or run sakana as a daemon.


8 Testing

make check runs unit tests for the configuration parser, the duration parser, the tolerance state machine, the /keys analysis, the error classification and the reporting command, and an integration suite that drives the real sakana binary against a fake exchange process.

The fake exchange, tests/fakeexchange, serves a synthetic /keys response for a named scenario. It mints its own certification authority at start-up, writes the CA certificate to a file and issues its server certificate from it, so the integration tests exercise real TLS without touching the system trust store: sakana is told to trust that file with --trust. The scenario can be changed while the process runs, by writing another scenario name into the file named by -mode-file, which is how the suite exercises failure sequences and the restoration report. tests/fakeexchange -list-modes lists the scenarios.

A few failures cannot be produced with a real socket in a sandbox without becoming flaky — a name that does not resolve, a host that drops packets. Those are covered by unit tests that substitute the dialer and return the operating system error in question, so the classification is still tested end to end from the error to the report text.

make check-live checks the public exchanges https://exchange.taler-ops.ch/ and https://exchange.demo.taler.net/ and expects them to pass without a report, with no horizon configured at all: defaults that warn about a correctly operated exchange would be useless defaults. It needs network access and is therefore not part of make check. The same expectation is pinned down offline by a unit test that analyzes an exchange of the demo exchange’s shape—denominations with a seven day window published about 30 days ahead, signing keys two months ahead—and requires silence.


9 The Debian package

make deb builds the package with dpkg-buildpackage. The packaging aims at a bit-identical result from the same source:

A fixed timestamp

debian/rules exports SOURCE_DATE_EPOCH as the commit date of the release, hard-coded in the file. contrib/update-source-date-epoch.sh sets it to the date of the current HEAD, which is how it is refreshed at release time.

A reproducible compiler invocation

CGO_ENABLED=0 keeps the C toolchain out, and -trimpath and -buildvcs=false keep the build directory and the git state out of the binary. The build cache is redirected into the build tree.

A reproducible archive

TZ=UTC and LC_ALL=C fix the environment, and dh_builddeb clamps the member timestamps to SOURCE_DATE_EPOCH.

make deb-reproducible builds the package twice in separate trees and compares the checksums, which is the only real proof.


10 Error class reference

ClassSeverityReported when
dns-failureerrorthe host name does not resolve
host-unreachableerrorno SYN-ACK and no RST, or ICMP unreachable
port-unreachableerrorthe connection was refused with a RST
tls-handshakeerrorthe handshake or the certificate validation failed
tls-expiringwarningthe certificate expires within TLS_RENEWAL_HORIZON
http-statuserror/keys did not return 200 OK
http-timeouterrorno complete response within TIMEOUT
keys-parseerrorthe response is not an exchange /keys object
signkeys-expirederrorno signing key is valid any more
signkeys-expiringwarningno signing key is valid beyond SIGNKEY_EXPIRATION_HORIZON
denoms-expirederrorno denomination can be withdrawn any more
denoms-expiringwarningno denomination can be withdrawn beyond DENOMINATION_EXPIRATION_HORIZON
global-fees-missingwarningglobal fees do not cover the required period
wire-fees-missingwarningwire fees of a method do not cover it

The severity only affects the wording of the report and the level of the log line. Warnings and errors use the same failure counters and both block the restoration report.


Index