Repository navigation
Releases: AndrianBdn/oddk
Release list
ODDK v0.1.94
Added
- Archives are encrypted at rest. Every snapshot, legacy backup and
pre-upgrade backup is now written as.tar.zst.age: age
with a post-quantum hybrid key (ML-KEM-768 + X25519) derived frommaster.key.
Someone who can read the backup directory or the offsite bucket but does not
havemaster.keylearns nothing about the contents, not even instance names.
There is no new secret: restore and apply use the--master-keythey already
take. There is no setting to turn it off. - A wrong master key is refused before anything changes, and the error names
the key the archive needs: the checksum that ends itsmaster.keyline.
snapshot applynow checks the key first, before any other preflight step. oddk snapshot decryptwrites the plain.tar.zstout of an encrypted
archive (verified first, mode 0600). With--print-age-identityit prints the
derived age secret instead, so the stockageCLI can decrypt with no ODDK
installed. It runs locally; no daemon needed.
Changed
- Archives written before this release keep their
.tar.zstname and stay
restorable through every path, unchanged. - An
--s3-uridownload that this host cannot decrypt is now discarded, because
it cannot be verified, rather than kept for a retry. Supply the source's
--master-keyand the retry downloads it again. - A legacy
backup restoreof a backup written on another host is refused:
backups have no--master-key. Snapshots are the cross-host path.
Action: without master.key, archives from this release on cannot be
restored or read. Confirm now that master.key is backed up somewhere other
than this host and its bucket.
Action: ODDK versions before 0.1.94 cannot read the new archives. Install
this release or later on any host you would restore or apply to.
Action (optional): archives already in your bucket stay unencrypted
until retention prunes them. To be rid of them sooner, delete them once a newer
encrypted complete snapshot exists (oddk snapshot list,
oddk snapshot remove-remote <id>).
ODDK v0.1.93
v0.1.93 — 2026-09-30
Fixed
snapshot restore-instance --jsonandsnapshot restore-database --json
print only JSON on stdout. Progress lines and credential notes ("Restoring
…", "No AWS credentials in this shell") used to come first, so a script
parsing the output failed after the restore had already run. A retry was
then refused, because the database existed, or rebuilt the instance a second
time. Those lines now go to stderr in JSON mode.--jsonon these two commands now requires--yes, as it already did for
migrate-from-backupsanddangerously-drop-all. The confirmation prompt
would otherwise land in the JSON output.
Action: add --yes to any script that runs either command with --json.
v0.1.92 — 2026-09-30
Deprecated
- Per-instance backups (
oddk backup) will be removed in the first release
after 2026-12-31. Snapshots now cover everything backups did, including
restoring or cloning a single database (oddk snapshot restore-database,
v0.1.87).- Every
oddk backupcommand prints a notice on stderr naming its
replacement. Stdout and--jsonoutput are unchanged, so scripts keep
working. - New backup schedules are refused. Existing schedules keep running and
can still be changed, paused, resumed or removed. - Restore, list, download and
dangerously-drop-allkeep working until the
removal, so existing archives stay usable.
- Every
Action: if oddk checklist shows a legacy backups warning, move the
schedules before the removal with oddk snapshot migrate-from-backups --dry-run
(preview), then --yes. When you trust the snapshot schedule, clear the old
archives with oddk backup dangerously-drop-all. Anything that creates backup
schedules through oddk backup setup-cron or POST /api/cron/backup must
switch to oddk snapshot setup-cron.
v0.1.91 — 2026-09-29
Fixed
- A whole-instance restore makes no connection that fires the restored
cluster's login triggers (PostgreSQL 17+). After a physicalrestore-instance
orsnapshot apply, the readiness check and database count connected
normally. After a logical one, the final verification did. Each of those
connections fired any login trigger in the restoredpostgresdatabase. An
audit trigger recorded them as user logins, and a trigger that raises an error
made a completed restore report failure. Once a restore finishes, the
cluster's triggers fire on logins as they always did, including ODDK's health
checks.
Action: None.
v0.1.90 — 2026-09-29
Fixed
- Restores no longer fire the database's own login event triggers
(PostgreSQL 17+).pg_restorerecreates a login trigger near the end of a
restore and then reconnects. That reconnect ran the trigger against the
database it had just restored. A trigger that deletes rows emptied the
restored database, and an audit trigger logged the restore as a user login.
Every restore now runs with event triggers off:backup restore,snapshot restore-database, a logicalrestore-instanceorapply, andinstance major-upgrade. - The scratch copy used by
snapshot restore-databasefires no login trigger
either, and its read-only setting can no longer be overridden by a role or
database setting inside the archive. Its clients set both at connect time,
where they take precedence. Before this, a login trigger could change the
copy before it was dumped. Even a harmless audit trigger made the restore
fail. - A restore extracts only bytes it verified. Since v0.1.89 a rewrite of the
archive during a restore was detected by its size and modification time. A
different archive of the same length with its timestamp preserved got past
that check. The bytes read for extraction are now hashed and must match the
bytes that were verified. This costs one SHA-256 pass over the archive per
restore.
Action: None.
v0.1.89 — 2026-09-29
Fixed
- A restore extracts exactly the archive it verified. The archive was
verified, closed, and then opened again by name to extract. A file replaced
in between, for example by a sync job or acpinto the backup directory,
passed the checks as one archive while another was restored. Verification
and extraction now read the same open file. An archive rewritten in place
while a restore reads it is refused, and the extracted files are discarded,
before anything is changed. This covers every restore:backup restore,
snapshot restore-instance,snapshot restore-database,snapshot apply
andinstance major-upgrade. - The scratch copy used by
snapshot restore-databasecannot be left behind
unnoticed. Docker can report a container removed while failing to delete
its volume, which here holds a full copy of an instance's data. The volume is
now created with an ODDK label and removed by a separate, checked call. If
that fails, the command says so and prints the removal command. The daemon
also removes any leftover scratch volume when it starts.
Action: None.
v0.1.88 — 2026-09-29
Fixed
- Database names are always passed to PostgreSQL tools as a name. A name
containing=, or starting withpostgres://or-, was read by
pg_restore,pg_dumpand libpq as a connection string or an option.
backup restore --restore-as 'dbname=postgres'could pass the "target does
not exist" check and then write into the existingpostgresdatabase. This
affectedbackup restore,instance major-upgrade, and backups and logical
snapshots of such a database. - A database whose name contains a space or other punctuation can be backed
up. The name was used in a helper container's name, which Docker rejects. - Restoring by
--idchecks the local copy against the recorded SHA-256.
Since v0.1.86 this was checked only when the archive had to be downloaded.
A catalogued local file that had been replaced by another valid archive was
restored as if it were the original. This coversbackup restore --id,
snapshot restore-instance --idandsnapshot restore-database --id. The
check runs in the verification pass that already happens before extraction,
so it adds no extra read. Archives recorded before v0.1.86 have no digest and
are not checked.
Action: None.
v0.1.87 — 2026-09-29
Added
oddk snapshot restore-databaserestores one database out of a snapshot.
It creates the database on a running instance and changes nothing else. The
instance's other databases, roles, postgres password and configuration stay as
they were, and it keeps serving. The target name must be free, so use
--restore-asto put yesterday's copy next to today's. It takes the same
--file,--idand--s3-urisources asrestore-instance. As withbackup restore, objects come back owned bypostgres, without object privileges.
Until now, restoring a single database still needed a per-instance backup.--from-instancerestores another instance's database. For example,
--instance staging --from-instance prod --database salesputs last night's
production database on staging. The target does not have to be in the
snapshot. It must be running a PostgreSQL major at least as new as the
snapshot's copy.- From a physical snapshot (the default format), the instance's copy is
started as a scratch cluster first, because a physical archive has no
per-database dumps. The database is dumped out of that cluster, and then the
cluster and its volume are removed. The scratch cluster has no network
access, and it runs no background workers, no logical replication and no
WAL archiving. So the source's subscriptions, foreign servers and scheduled
jobs (pg_cron, TimescaleDB and pg_partman workers) can neither reach real
systems nor change the copy before it is dumped. It needs free disk for the
whole cluster while it runs, and UNLOGGED tables come back empty.
Fixed
- Helper containers no longer leave a Docker volume behind. Every
pg_dump,
pg_restoreandpg_basebackuphelper got an anonymous volume from the
postgres image, and removing the helper did not remove it. A host that has run
nightly backups for a while has thousands of them. They are small, but each is
a directory and a Docker record.
Action: optional cleanup. ODDK cannot tell its leaked volumes from anyone
else's because they carry no label, so it does not remove existing ones. List
them with docker volume ls -qf dangling=true. If nothing else on the host
relies on unattached anonymous volumes, reclaim them with docker volume prune.
On Docker 23 and later, that command removes only anonymous volumes.
v0.1.86 — 2026-09-29
Added
- Every new backup and snapshot records its SHA-256 in the catalogue. The
digest is of the archive file, the same valuesha256sumprints. It is taken
from the read-back that already verifies every archive before it is
published, so it describes bytes that were proven intact. It appears as
sha256insnapshot list --json,snapshot make --json, and the backup
records returned byGET /api/backups. - Downloading a catalogued archive checks it against that digest. This
coversbackup download,snapshot download <id>, andsnapshot restore-instance --idwhen it has to fetch the archive. Corruption was
already refused. What is new is refusing an archive that is intact but is
not the one this host wrote: an object replaced in the bucket, for example.
Previously that downloaded cleanly and would have restored someone else's
data. The download is now discarded, and the error gives both digests.
--s3-urifetches are not tied to a catalogue row, so they are not checked.
Archives recorded before this version have no digest. They are not checked,
and each one records the digest of its first verified download instead. A
recorded digest is ...
ODDK v0.1.85
Fixed
- Backups and snapshots larger than 5 GiB can upload to S3. Large archives
upload in parts, with retries per part and cleanup on failure or cancellation.
Uploads stream from disk with two concurrent parts. - Local retention protects large snapshots that have no offsite copy. The
previous exception for archives above 5 GiB is removed. Migration previews no
longer warn about the old upload cap.
Action: Upgrade ODDK and restart its daemon. Allow s3:AbortMultipartUpload
on the archive prefix in the bucket's upload policy. Configure an S3 lifecycle
rule to abort incomplete multipart uploads left by crashes or failed cleanup.
For SSE-KMS buckets, the upload identity also needs kms:Decrypt alongside
kms:GenerateDataKey on the encryption key. No database migration is required.
ODDK v0.1.84
Fixed
- Deleting a database user no longer risks deleting objects created during
ownership reassignment. Each database's reassignment and privilege cleanup
now run in a transaction that refuses object deletion. If concurrent DDL would
cause data loss, the command preserves the object and the role and asks you to
stop the application's DDL before retrying. Cleanup already completed in other
databases is not rolled back. - Container rollback uses the previous image's immutable ID. If an update
or image switch moves a tag to a broken image, rollback restores the image the
old container actually used instead of retrying the broken tag. The previous
image is checked before the existing container is removed. - Adding a database user is atomic. Role creation, grants, and optional
ownership transfer now share one transaction. A failed grant or ownership
change no longer leaves a partially configured user or changed database owner
behind without returning the generated password. add-db-user --ownerhandles stored procedures and aggregates, as well as
functions. Ownership statements explicitly name the public schema, so a
customizedsearch_pathcannot redirect them.
Changed
- Parameter-file decoding is shared by the CLI and API; both still accept a
bare array or the object emitted byparameters get --json. S3-location
parsing is shared by backup and snapshot operations and rejects locations
with an empty bucket or object key. - Removed the E2E runner's unused
-vand-parallelflags.
Action: Upgrade ODDK and restart its daemon before managing database users
or changing instance images. No database migration is required.
ODDK v0.1.83
Added
oddk instance applynow resizes and re-ports an instance, not only its
parameter group.--cpu,--ramand--portcan be given in any
combination with--parameter-group; flags you do not pass keep their
current value. The change goes through the same path as a group change: the
container is recreated (a brief restart, the data volume untouched), what can
be refused is refused before the running container is touched — more CPU or
RAM than the host has, a port another instance holds or something already
answers on, a group whose shared memory will not fit the new RAM — and a
container that comes up but never reaches readiness is rolled back to the
previous shape. Because parameter groups resolve against the instance's RAM,
a resize re-derivesshared_buffersand friends automatically. Until now CPU,
RAM and port were fixed atcreatefor the life of the instance, which was
the biggest gap against the "local RDS" positioning.
PUT /api/rdbms/{name}/configaccepts the new fields (cpuCores,ramMb,
port, all optional); a body with onlyparameterGroupbehaves as before.
Security
- Built with Go 1.27.1 (v0.1.82 was built with 1.26.5). A
govulncheckof
the previous binary reported six standard-library vulnerabilities as
reachable: quadratic path resolution innet/url, unbounded post-handshake
messages incrypto/tls, two innet/http(the unencrypted HTTP/2 check
ignoringReadHeaderTimeout, and Punycode label handling), and missing
recursion guards inencoding/xmlandencoding/asn1. Exposure was limited
by design — the daemon listens on loopback and only talks to Docker over its
socket, to S3, and to the notification endpoints you configured — but fixing
all six costs one line.golang.org/x/textmoves to 0.39.0 for the same
reason (an infinite loop on invalid input).
Changed
- The Docker client library is now
github.com/moby/moby/client, the
client Docker Engine 29 ships, replacinggithub.com/docker/docker. That
module stopped receiving releases at 28.5 and carries two Moby
plugin-authorization advisories with no fix on its import path. ODDK never
ran the affected code — it only talks to the daemon over its socket — but a
vulnerability report that can never go green is one nobody reads. Nothing
changes for a deployment: the same API calls are made, and the client still
negotiates the API version with whatever Docker Engine is installed. - Every direct dependency is on its current release. The notable jumps: the
SQLite driver (modernc.org/sqlite1.38 → 1.58), the CLI framework
(urfave/cli3.4 → 3.11), the AWS SDK's S3 client (1.97 → 1.110),pgx
5.9 → 5.10 andklauspost/compress1.18 → 1.20. No behaviour change is
intended. Ifoddkmisbehaves after this upgrade in a way these notes do not
explain, the SQLite driver bump is the first suspect.
Fixed
- An authentication failure is recognised by its SQLSTATE, not by the English
text of the server's message. PostgreSQL localizes server messages through
lc_messages, which any parameter group can set, so on a cluster running
with a non-English locale the probe behindoddk instance statusand the
checklist reportedPostgreSQL connectivity issue (other)where it should
have saidPostgreSQL authentication failed, andadd-db-useron a missing
database returned a generic error instead of the 404 with thecreate-db
hint. S3 "no such object" and Docker "no such container" answers are now
recognised by their typed errors as well. Nothing changes where the messages
were English. oddk snapshot applyrefuses a truncated--logicalarchive before it
replaces anything. A logical archive missing itsdatabases.jsonor
globals.sqlwas discovered only while rebuilding the instance, which is
afteroddk.dbandmaster.keyhad been installed — an apply that could
not simply be retried. The physical format already had this check in
preflight; the logical one now does too, and bothapplyand
restore-instancebuild a cluster through one shared function, so the two
cannot drift apart on this again.oddk backup remove-remoteandoddk snapshot remove-remoteno longer fail
with a bareEOFwhen the S3 delete stalls past the daemon's 30-second
response deadline; the removal completed on the daemon either way. The rule
behind this and thedelete-db-userfix below is now enforced by a test, so
a new long-running handler cannot ship without it.oddk instance delete-db-userno longer fails with a bareEOFon a large
database.REASSIGN OWNED BYlocks every object the user owns in one
transaction and could outlive the daemon's 30-second response deadline; the
operation then completed on the daemon while the CLI reported a connection
error, leaving you to check whether the user was actually gone. The handler
now clears the deadline like every other long-running operation.install.shno longer tells the owner of a legacy/home/oddkinstall to
run a relocation script that was retired in v0.1.45. It says what has to
move instead.
ODDK v0.1.82
Fixed
- An instance left in
errorno longer displays asstopped.oddk list,
oddk instance statusand the per-instance header inoddk checklistderive
what they show from the container, which is what makes a database somebody
stopped out of band readstoppedrather thanrunning. But it also
overwroteerror— the one status that is never cleared on its own — so an
operation that failed and whose rollback failed left the row sayingerror
while every list command saidstopped, the word for an instance stopped on
purpose. The sameoddk checklistrun printedinstance_error:<name>and a
failing health line two lines away from the header contradicting them, and
nothing cleared the row, so it read that way indefinitely. Health checks and
notifications were always driven by the stored value and were unaffected.
Changed
-
oddk instance major-upgradenow pulls the target image itself if it is
missing, likecreate,switchandupdatealready did, instead of
stopping at the confirmation prompt to send you away for a separate
oddk pull. The pull happens after you confirm, so a cancelled upgrade
fetches nothing, and an upgrade that cannot happen on version grounds — a
downgrade, or the major the instance already runs — is still refused
instantly, without fetching anything.One message changed with it: a
--target-versionwhose image exists in no
registry now fails naming the image that could not be resolved, rather than
reporting it missing locally and advising a pull that would have failed the
same way.
ODDK v0.1.81
Changed
-
An instance's postgres password is no longer written into its container's
Docker configuration. Docker keepsConfig.Envfor a container's whole
lifetime and hands it to anything that can read container metadata, so the
credential was reachable throughdocker inspect, a backup of
/var/lib/docker, or an observability agent holding a read-only
docker.sockmount. A new cluster is now initialised with a throwaway
password and the real one is set over SQL once it is ready, so what remains
visible authenticates nothing. Recreating a container (instance apply,
switch,update) passes no password at all.This is hygiene, not a closed exposure: anyone who can read that metadata can
alsodocker exec <container> psql -U postgres, which the cluster answers
over its local socket without a password. No action is required.Containers created before this release keep their old value until their next
recreate — clearing it sooner would mean restarting the database purely to
tidy metadata.oddk instance update <name>will do it if you want it gone
now, at the cost of a brief restart. -
A recreate that meets an unexpectedly empty data volume now refuses to start
instead of initialising a fresh empty cluster in place of your data.