Releases: elroykanye/barme
Release list
v1.1.0
barme 1.1.0
A deployment can now tell the console where the API and the CDN really are.
Additive over 1.0: new optional settings, no change to the on-disk format and no
change to any existing behaviour.
In this release
-
console_api_url/console_cdn_url(#11). The console worked out its API
base from the address bar plus a fixed port. That's right when it's reached
directly and wrong the moment anything is put in front of it: served from
https://store.example.comit calledhttp://store.example.com:7373— a port
a reverse proxy doesn't publish, over plain HTTP from an HTTPS page, which
browsers block as mixed content even when the port is open. Sign-in then never
completed, and because the blocked request breaks the padlock it read as a TLS
fault, sending you to inspect a certificate that was perfectly fine.Nothing outside the server could fix it:
VITE_BARME_APIis a Vite
compile-time constant and the console is baked into the binary, so changing it
meant rebuilding the image;cors_originscovers only the CORS half; and
barmed knew its bind addresses but had no notion of a public URL. Now it does.
Setconsole_api_urlandconsole_cdn_urlinbarme.toml(or
BARME_CONSOLE_API_URL/BARME_CONSOLE_CDN_URL) and they're injected into
index.htmlas it is served — at serve time, not build time, which is the
whole point: a deployment sets them without rebuilding an image. The console
prefers them over what it would otherwise derive.The values land inside a script tag, so they're escaped rather than trusted,
with a test that a value can't close the tag it sits in.
Compatibility
Drop-in over 1.0.0. Leave the new settings unset and not a byte of the served
page changes — the console behaves exactly as before, which is what you want on
localhost. Same on-disk format, same stable API (see docs/STABILITY.md).
Upgrading behind a proxy
If you publish the console through a reverse proxy, set the two URLs and route
those paths to the native (7373) and CDN (7375) listeners:
console_api_url = "https://store.example.com/api"
console_cdn_url = "https://store.example.com/cdn"
Docker
docker run -p 7373:7373 -p 7374:7374 -p 7375:7375 -p 9000:9000 \
-e BARME_MASTER_KEY=$(openssl rand -hex 32) \
-v barme:/data elroykanye/barme:1.1.0
The road here
- 0.4.x — durability (fsync, crash recovery) and concurrency/GC hardening
- 0.5.x — security (encrypted secrets, no default login, presign, CORS)
- 0.6.0 — S3 multipart upload
- 0.7.0 — on-disk format version + API freeze
- 0.8.0 — S3 bucket operations, Helm chart
- 0.9.0 — operability (backup/restore, readiness, metrics, name fuzzing)
- 1.0.0 — frozen format and API, trustworthy with data, operable
- 1.1.0 — the console reachable from behind a reverse proxy
v1.0.0
barme 1.0.0
barme is 1.0. It makes three promises and keeps them:
- The on-disk format and API are frozen. Anything written to a 1.x server
reads on every later 1.x. Format changes ride a version stamp and a migration,
never a silent break. - You can trust it with data. An acknowledged write survives a hard kill
(fsync-durable, recovers on restart); concurrent writes and GC are safe under
load; secrets are encrypted at rest; there's no standing default credential. - It's operable. Liveness + readiness probes, Prometheus
/metrics, a
documented and tested backup/restore story, and a Helm chart.
Everything is proven by harnesses that run in CI territory, not just asserted:
crash/kill durability, GC-under-load, security posture, multipart abuse, and a
copied-data-dir restore.
In this release
Two delivery-correctness fixes closed before cutting 1.0:
/cdn/{hash}erasure caveat, documented (#6). The immutable, cache-forever
hash URL can't be revoked once bytes are in a cache, so deleting at the origin
can't pull them back. This is now written down clearly —/cdnis for public,
non-erasable content; serve erasable or personal data over the short-lived
/s/{pot}/{key}share instead. Documented in USAGE, STABILITY, and the code.- Object hash surfaced on every write path (#7). A new
X-Barme-Object-Id
response header carries the object's content id on single PUT, multipart
complete, and HEAD — the reliable handle for a/cdnlink, since an S3
multipart ETag is a digest of part digests, not the object hash.
The road here
- 0.4.x — durability (fsync, crash recovery) and concurrency/GC hardening
- 0.5.x — security (encrypted secrets, no default login, presign, CORS)
- 0.6.0 — S3 multipart upload
- 0.7.0 — on-disk format version + API freeze
- 0.8.0 — S3 bucket operations, Helm chart
- 0.9.0 — operability (backup/restore, readiness, metrics, name fuzzing)
- 1.0.0 — the delivery caveats above, and meaning the three promises
Compatibility
Drop-in over 0.9.0. This is the compatibility baseline: 1.x won't break 1.0 data
or the stable API (see docs/STABILITY.md).
Docker
docker run -p 7373:7373 -p 7374:7374 -p 7375:7375 -p 9000:9000 \
-e BARME_MASTER_KEY=$(openssl rand -hex 32) \
-v barme:/data elroykanye/barme:1.0.0
Scope and what's next
1.0 is a single node you'd trust. Not in 1.0, by design: horizontal
distribution (the v2 headline — content-addressing makes replication cheap to add
later), encryption of object contents, and the experimental surfaces (semantic
search, sync, webhooks, image-codec transcoding) that stay marked as such.
v0.9.0
barme 0.9.0
The operability pass: barme is now something you can watch, probe, and back up
with confidence — the last of the three v1 promises ("run it, watch it, back it
up, recover it"). The remaining gaps before 1.0 are closed.
What changed
- Backup and restore, documented and proven. The data directory is a
self-contained, coherent backup target;docs/USAGE.mdcovers how to snapshot
it, where the master key must live, and how to restore. A test copies a
populated data dir elsewhere, reopens it, and confirms every object reads back
byte-for-byte with history and integrity intact. - Readiness endpoint.
GET /readyreturns 503 when the store can't be read,
distinct fromGET /health(liveness). The Helm chart's readiness probe now
points at it. - Richer metrics.
/metricsadds GC counters — sweeps, chunks condemned,
chunks erased, and reachable chunks from the last sweep — plus logical bytes
alongside physical, so dedup savings and GC activity are visible to Prometheus. - Server-side verify in the crash harness. After every crash, the harness now
cross-checks each acknowledged object against the server's ownPOST /verify,
so recovery (download + re-hash) and the store's integrity check must agree. - Fuzzed name handling. A property test throws arbitrary unicode, slashes, and
lengths straddling the 255-byte filename bound atput/get: they either
round-trip or reject cleanly, never panic. - Structured request logs. Requests carry
method/uri/status/latency
through the HTTP trace layer, kept at debug so health probes don't spam the log;
enable withRUST_LOG=…,tower_http::trace=debug.
Compatibility
Drop-in over 0.8.0. New endpoint (/ready) and metrics only; on-disk format and
the stable API are unchanged. Helm chart bumped to 0.2.0 / appVersion 0.9.0.
Docker
docker run -p 7373:7373 -p 7374:7374 -p 7375:7375 -p 9000:9000 \
-e BARME_MASTER_KEY=$(openssl rand -hex 32) \
-v barme:/data elroykanye/barme:0.9.0
Known limits
- Alpha, but close: with this pass the v1 blocks (durability, security, format +
API freeze, ops) are all done. A 1.0 is a decision away. GET /{pot}(S3 ListObjects) is still not implemented; use native
/pots/{pot}/objects.- Object contents aren't encrypted (secrets at rest are).
- Single node. Image codecs (JPEG XL, AVIF) are routed but not yet transcoding.
v0.8.0
barme 0.8.0
Bucket-level S3 operations and a Helm chart — barme is now something you can
point an S3 client's full preflight at, and deploy to Kubernetes in one command.
What changed
-
S3 bucket operations. The S3 door gained the verbs it was missing:
PUT /{pot}— CreateBucket (idempotent; a repeat is 200)HEAD /{pot}— HeadBucket (200 if it exists, 404 otherwise)DELETE /{pot}— DeleteBucket (409 if the pot still holds objects, else 204)GET /— ListBuckets (XML list of every pot)
Pots stay implicit for writes — a first PUT to an unknown pot still lands with
the default policy — but these give a pot an explicit identity when a client
provisions or lists one. A pot exists if it has a config or holds objects.
DeleteBucket refuses a non-empty pot and does the empty-check and delete
atomically (under the commit locks), so a write racing the delete can't have
its just-acknowledged object silently wiped. -
Helm chart (
charts/barme).helm install barme ./charts/barmeruns a
single-node store on Kubernetes: Deployment (one replica,Recreate, since the
data volume is ReadWriteOnce), a PVC for/data, a Secret holding the owner
credential and master key, a Service for all four doors, and an optional
(SigV4-safe, no path-rewrite) ingress. Blank credentials are generated on first
install and preserved across upgrades. Argo CD /helm templateusers: pin
auth.masterKey(or useauth.existingSecret) — see the chart README, a
regenerated master key makes stored secrets unrecoverable.
Compatibility
Drop-in over 0.7.0. New endpoints only; the 0.7.0 on-disk format and stable API
are unchanged. docs/STABILITY.md lists the bucket verbs under the (S3-tracks-AWS)
stable surface.
Docker
docker run -p 7373:7373 -p 7374:7374 -p 7375:7375 -p 9000:9000 \
-e BARME_MASTER_KEY=$(openssl rand -hex 32) \
-v barme:/data elroykanye/barme:0.8.0
Known limits
- Alpha. Formats may still shift before 1.0 — don't store anything you can't lose.
GET /{pot}(S3 ListObjects) is not implemented yet; use the native
/pots/{pot}/objectsto list.- The master key protects secrets at rest; object contents aren't encrypted.
- Doors bind
0.0.0.0; reach a local instance via127.0.0.1(IPv4). - Single node. Image codecs (JPEG XL, AVIF) are routed but not yet transcoding.
v0.7.0
barme 0.7.0
Format and API freeze — the groundwork that lets barme evolve without orphaning
data or breaking clients, and the last big block before a v1 that means something.
What changed
- On-disk format is versioned. A
format.jsonat the data root stamps the
layout version. On open, a directory written by a newer barme is refused
(rather than risk misreading a layout it doesn't know), an older one runs its
migration and rewrites the stamp (the hook is in place; no steps needed yet),
and a pre-0.7 directory is adopted as v1 with its data intact. The resolved
version is logged at startup. - Manifests enforce their version. Every object already carried a
manifest_version; reads now refuse a manifest newer than this build
understands, per object, before trusting its fields. - The v1 manifest schema is pinned. A test hashes a fully-fixed manifest and
asserts its content address, so any silent change to the schema fails CI and
forces a conscious version bump. - The API surface is frozen and documented.
docs/STABILITY.mdstates the
compatibility promise and lists the stable surface (objects & versions, pots,
keys, the S3 and CDN doors, presign, ops) versus the experimental surface
(semantic search, sync, webhooks, image codecs), which is now marked as such in
the live/docs.
Compatibility
Drop-in. An existing data directory is stamped format v1 on first open under
0.7.0 and keeps working unchanged. From here on, within a major version, data and
the stable API don't break — see docs/STABILITY.md.
Docker
docker run -p 7373:7373 -p 7374:7374 -p 7375:7375 -p 9000:9000 \
-e BARME_MASTER_KEY=$(openssl rand -hex 32) \
-v barme:/data elroykanye/barme:0.7.0
Known limits
- Alpha. The freeze is the intended contract and is largely in force, but formats
may still shift before 1.0 — don't store anything you can't lose yet. - The master key protects secrets at rest; object contents aren't encrypted.
- Doors bind
0.0.0.0; reach a local instance via127.0.0.1(IPv4), not
localhost. Put barme behind a firewall or reverse proxy. - Single node. Image codecs (JPEG XL, AVIF) are routed but not yet transcoding.
v0.6.0
barme 0.5.1
Finishes the security block: the CORS config knob now works, and expiring share
links are verified end to end.
What changed
cors_originsis now enforced. It was a dead config field — both the API
and CDN doors hardcoded permissive CORS, so any website could script the API
from a visitor's browser regardless of config. Now["*"](the default) stays
permissive for local use, but a specific list restricts
Access-Control-Allow-Originto exactly those origins. Set your console/app
origins in production.- Presigned share links, verified. The CDN share door (
/s/{pot}/{key})
checks the HMAC signature and the expiry, serving a private object only for a
valid, unexpired link. Now covered by tests: a valid link serves; an expired
one, a tampered signature, and a signature minted for a different key are each
refused with403.
With these, the v1 security block is complete: no default login, secrets
encrypted at rest, constant-time auth compare, verified share links, and a real
CORS boundary.
Upgrading
Drop-in. If you relied on the API being reachable from any browser origin, that
still works by default (cors_origins = ["*"]); set an explicit list to lock it
down.
Docker
docker run -p 7373:7373 -p 7374:7374 -p 7375:7375 -p 9000:9000 \
-e BARME_MASTER_KEY=$(openssl rand -hex 32) \
-v barme:/data elroykanye/barme:0.5.1
Known limits
- Alpha. Formats and on-disk layout may still change before v1.
- The master key protects secrets at rest; it does not encrypt object contents.
- Doors bind
0.0.0.0by default — put barme behind a firewall or reverse proxy;
reach a local instance via127.0.0.1(IPv4), notlocalhost. - A pot name plus key must encode to under 255 bytes (about 120 key bytes for a
short pot). - Single node. Image codecs (JPEG XL, AVIF) are routed but not yet transcoding.
v0.5.1
barme 0.5.1
Finishes the security block: the CORS config knob now works, and expiring share
links are verified end to end.
What changed
cors_originsis now enforced. It was a dead config field — both the API
and CDN doors hardcoded permissive CORS, so any website could script the API
from a visitor's browser regardless of config. Now["*"](the default) stays
permissive for local use, but a specific list restricts
Access-Control-Allow-Originto exactly those origins. Set your console/app
origins in production.- Presigned share links, verified. The CDN share door (
/s/{pot}/{key})
checks the HMAC signature and the expiry, serving a private object only for a
valid, unexpired link. Now covered by tests: a valid link serves; an expired
one, a tampered signature, and a signature minted for a different key are each
refused with403.
With these, the v1 security block is complete: no default login, secrets
encrypted at rest, constant-time auth compare, verified share links, and a real
CORS boundary.
Upgrading
Drop-in. If you relied on the API being reachable from any browser origin, that
still works by default (cors_origins = ["*"]); set an explicit list to lock it
down.
Docker
docker run -p 7373:7373 -p 7374:7374 -p 7375:7375 -p 9000:9000 \
-e BARME_MASTER_KEY=$(openssl rand -hex 32) \
-v barme:/data elroykanye/barme:0.5.1
Known limits
- Alpha. Formats and on-disk layout may still change before v1.
- The master key protects secrets at rest; it does not encrypt object contents.
- Doors bind
0.0.0.0by default — put barme behind a firewall or reverse proxy;
reach a local instance via127.0.0.1(IPv4), notlocalhost. - A pot name plus key must encode to under 255 bytes (about 120 key bytes for a
short pot). - Single node. Image codecs (JPEG XL, AVIF) are routed but not yet transcoding.
v0.5.0
barme 0.5.0
Security. barme no longer ships with a working default login, and access-key
secrets are encrypted on disk instead of sitting in plaintext. This closes the
"don't expose it yet" caveat that stood since the first release.
What changed
- No more default
barme/barme. The built-in credential is gone. On a fresh
store with no credential configured, the daemon mints a random owner and prints
it once at startup — save it, it isn't shown again. Set your own with
BARME_ACCESS_KEY/BARME_SECRET_KEY(or[credentials]inbarme.toml) and
that's used instead. An exposed barme is no longer a known-password login. - Secrets encrypted at rest. Access-key secrets are stored AES-256-GCM
encrypted, not as plaintext JSON. Hashing isn't an option here — the S3 door
verifies AWS SigV4, a symmetric HMAC, so the server must recover the raw secret
to check a signature (the same reason AWS and MinIO keep recoverable secrets).
Encryption keeps the key store free of plaintext while preserving S3
compatibility: the secret is decrypted into memory only to verify a request. - Master key. The encryption key is resolved from
BARME_MASTER_KEY(64 hex
chars), else amaster.keyfile in the data dir (created0600on first boot),
else freshly generated and announced. For real deployments set
BARME_MASTER_KEYin the environment — keeping the key out of the data dir
protects a stolen backup too. Back the key up: encrypted secrets can't be
recovered without it. - Legacy plaintext key stores migrate automatically. An existing store opened
with a master key re-encrypts any plaintext secret records in place on startup. - Constant-time secret comparison on the native door's Basic auth, closing a
timing side channel where a==leaked how many leading bytes of a guess were
right.
Verified
- SigV4 still round-trips end to end against a secret that never touches disk in
plaintext — encryption didn't break S3 compatibility. - Durability and concurrency unaffected: the crash, abuse, and GC-under-load
harnesses all pass on the new binary (every acknowledged object survives every
crash; ~180 objects intact undergrace=0GC). - New tests cover encrypt/decrypt, wrong-key and tampered-ciphertext rejection,
no-plaintext-on-disk, and legacy migration.
Upgrading
Mostly drop-in, with one thing to know:
- A
master.keyis created in your data dir on first boot (or set
BARME_MASTER_KEYyourself beforehand). Back it up. If you already had access
keys, their secrets are migrated to encrypted form on this first start. - If you relied on the
barme/barmedefault, setBARME_ACCESS_KEY/
BARME_SECRET_KEYexplicitly, or grab the auto-generated credential from the
startup output.
Docker
docker run -p 7373:7373 -p 7374:7374 -p 7375:7375 -p 9000:9000 \
-e BARME_MASTER_KEY=$(openssl rand -hex 32) \
-e BARME_ACCESS_KEY=you -e BARME_SECRET_KEY=change-me \
-v barme:/data elroykanye/barme:0.5.0
(Or run with no credential set and read the generated one from the logs.)
Known limits
- Alpha. Formats and on-disk layout may still change before v1.
- The master key protects secrets at rest; it does not encrypt object contents.
- A pot name plus key must encode to under 255 bytes (about 120 key bytes for a
short pot). barmedbinds IPv4 (0.0.0.0); on a dual-stack host reach it via127.0.0.1
rather thanlocalhost.- Single node. Image codecs (JPEG XL, AVIF) are routed but not yet transcoding.
v0.4.2
barme 0.4.2
More hardening from the same stress campaign that produced 0.4.1. Three
resilience and concurrency fixes, each with a regression test.
Fixes
- A corrupt condemned-set file no longer wedges GC. The
.condemnedfile
(chunk -> when-condemned) was deserialized with the error propagated, so a
single bad byte in it would fail every future sweep — GC would stop forever and
the disk would fill without bound. It now heals to empty: the set is disposable
derived state (mark re-derives reachability every pass; the stamps only gate the
grace window), so the worst case is a one-grace-period delay in reclaiming
chunks. This matches how the rest of the collector re-derives its own truth. - Concurrent delete and put on one key can't lose the delete.
deletenow
takes the same per-key commit lock that writes do. Without it a delete could
interleave with a put's read-modify-write and be silently undone (the put reads
the history, the delete removes the file, the put rewrites it and resurrects the
key). Serializing makes it clean last-writer-wins. - A corrupt pointer line no longer orphans the whole key. The pointer file is
the one mutable, non-content-addressed piece of state, so it's the one place a
disk bit-flip isn't caught by an address check. A single unparseable line used
to fail the entire read, taking the key's intact versions and rollback down with
it. Corrupt lines are now skipped, so the surviving versions stay readable. (A
corrupt current line resolves to the previous version rather than erroring —
availability over a hard failure, consistent with the rest of the store.)
Under the campaign, holding up
- A version-explosion probe (5000 writes to one key) showed flat ~per-write
cost, no O(n²) cliff — the pointer rewrite is dominated by the per-write fsync,
not by history length. See known limits for the one caveat at extreme counts. - New adversarial tests: racing put/delete on one key never corrupts state;
corrupt condemned set and corrupt pointer line both recover.
Upgrading
Drop-in. On-disk format unchanged; a 0.4.x (or 0.3.x) data dir opens as-is.
Docker
docker run -p 7373:7373 -p 7374:7374 -p 7375:7375 -p 9000:9000 -v barme:/data elroykanye/barme:0.4.2
Known limits
- Alpha. Formats and on-disk layout may still change before v1.
- Secret keys are still stored in the clear and the default
barme/barmelogin
still works out of the box — both land in the next release. Don't expose it yet. - A single key's version history has no cap by default (set
max_versionsper pot
to bound it). The pointer file grows linearly with versions and is rewritten per
write, so a key written millions of times becomes a large, slow file. A default
cap is planned. - A pot name plus key must encode to under 255 bytes (about 120 key bytes for a
short pot). barmedbinds IPv4 (0.0.0.0); on a dual-stack host reach it via127.0.0.1
rather thanlocalhost.- Single node. Image codecs (JPEG XL, AVIF) are routed but not yet transcoding.
v0.4.1
barme 0.4.1
Correctness under concurrency and garbage collection, found by an aggressive
stress campaign against 0.4.0. Three real defects fixed, all with regression
tests and adversarial harnesses.
Fixes
- Concurrent writes to one key no longer lose versions. The pointer file is
read-modify-write, and it had no serialization: 24 threads writing the same key
landed 1 version in history, not 24 — 23 acknowledged writes silently
dropped. Writes to a key now take a per-key commit lock (sharded, so different
keys still run fully parallel and chunking stays off the lock). All 24 versions
are now kept. This directly backs the store's "every write keeps the previous
version" promise. - GC can no longer erase an in-flight upload's chunks. A streaming upload
writes all its chunks before committing its pointer, so those chunks are
unreferenced for the whole upload. GC's own module doc promised a guard against
reaping young chunks, but the guard wasn't implemented — with a tight grace
window a sweep could condemn and erase a live upload's chunks, then the client
got a success for an object missing its bytes. Uploads now pin each chunk
the instant it's stored and unpin once the pointer commits; GC treats pinned
chunks as reachable, so a sweep can't touch them no matter how aggressive the
grace window. A crash still drops the pins, so a crashed upload's orphans are
collected normally. - Malformed pot names return 400, not 500. A pot name containing a slash or
..(e.g./objects/..%2F..%2Fescape/k) is rejected safely by the store, but
the door surfaced it as a 500. It's a client error and now answers 400 on both
the native and S3 doors. (The store already contained the input — nothing
escaped; this is about the right status code.) barmedreports the right version. The daemon crate pinned its own
0.3.0instead of inheriting the workspace version, so the 0.4.0 binary
reported0.3.0. It now tracks the release version.
Hardening (tests + harnesses)
scripts/crash-test.shgained a hot-GC mode (GC_GRACE=):kill -9mid-write
while GC actively sweeps and erases. Verified: 8 crash cycles under a 2s grace
with 1s sweeps, 62 objects, zero lost.scripts/abuse-test.sh: a battery of hostile HTTP inputs (overlong keys, path
traversal in keys and pot names, control chars, empty/at-cap/over-cap bodies,
bad auth, malformed JSON, huge headers) plus uploads undergrace=0GC. The
server survives every input and kept all ~290 objects intact.- New unit/integration tests: in-flight pin survival under aggressive GC, orphan
collection still works, and concurrent same-key / distinct-key write invariants.
Upgrading
Drop-in. On-disk format unchanged; a 0.4.0 (or 0.3.x) data dir opens as-is.
Docker
docker run -p 7373:7373 -p 7374:7374 -p 7375:7375 -p 9000:9000 -v barme:/data elroykanye/barme:0.4.1
Known limits
- Alpha. Formats and on-disk layout may still change before v1.
- Secret keys are still stored in the clear and the default
barme/barmelogin
still works out of the box — both land in the next release. Don't expose it yet. - A pot name plus key must encode to under 255 bytes (about 120 key bytes for a
short pot). barmedbinds IPv4 (0.0.0.0); on a dual-stack host reach it via127.0.0.1
rather thanlocalhost.- Single node. Image codecs (JPEG XL, AVIF) are routed but not yet transcoding.