v1.9.0
Bucket isolation gets its remaining pieces, the image drops from 1.88 GB to 205 MB, and the auth path becomes observable.
Upgrade note: this release refuses to start with a weak
TOKEN. See the bottom of these notes before deploying.
Security
-
The general
TOKENis now checked for strength at boot. It was previously only checked for emptiness, so a one-character token or a copied placeholder started the service and was accepted on every authenticated endpoint, operator routes included. It must now be at least 32 characters, the same floor bucket-scoped tokens already had, and must not be a template placeholder. See the upgrade note. -
Rejected credentials and denied bucket access are now logged. Previously invisible: a credential-stuffing run, a misconfigured client and a scoped token probing other tenants all produced an error response and no record. Three events at
warn, each with the method, path and Cloudflare-aware trusted client IP:auth.failurewhen a credential is rejected, with the reasonauth.scoped_token_on_operator_routewhen a valid bucket-scoped token reaches/aws/*,/minio/*,/monitoror/metrics, recorded separately because that is almost always a misconfigured client rather than an attackauth.bucket_access_deniedwhen a scoped token names another bucket, with both bucket names
No credential material is logged, in any form. Successful authentication is deliberately not logged: on a CDN that is the hot path and the access log already covers it.
-
The production image no longer contains a build toolchain. No compilers, no
-devheaders, no source tree, no repository checkout.
Added
- Optional expiry for bucket-scoped tokens. An entry may carry an
expires_atRFC 3339 timestamp; omitting it means the token never expires, so existing files are unaffected. Expiry is evaluated per request rather than at load time, which matters under the read-once-at-boot model: a token stops working the moment it expires instead of lasting until the next restart. An expired token is rejected exactly like an unknown one. A mistyped timestamp fails the boot rather than being read as "no expiry", and the boot log warns about entries that have already expired or expire within a week. - Bucket-scoped tokens can be deployed on Kubernetes.
k8s/secrets.yamlgained atokens_jsonkey, projected byk8s/deployment.yamlinto the pod at/app/config/tokens.jsonfrom a read-only, optional secret volume. It holds credentials, so it is a Secret and not a ConfigMap. While wiring it,TOKENitself turned out never to have been wired into the deployment at all, which meant the manifests could not boot; it now comes fromcdn-secrets.app_token.
Changed
-
The image is built in two stages and is 9x smaller: 1.88 GB → 205 MB. The single-stage build shipped everything used to produce the binary, including 251 MB of Go toolchain. The runtime stage now starts from
debian:bullseye-slimand receives only the binary,public/, the ImageMagick shared libraries and modules, and the runtime library closure, which was derived withlddover the binary and every coder module and mapped to packages withdpkg -S.ca-certificatesis installed explicitly: it is absent from the slim base, and without it every outbound HTTPS call would fail x509 verification.The hardened ImageMagick policy carries over. Because losing it would disable the hardening silently, CI now asserts the policy is live in the image that ships and that PNG, JPEG, WebP, GIF and TIFF still decode there while the PDF coder stays blocked.
-
ImageMagick is pinned instead of tracking the newest upstream release. Two builds of the same commit could previously ship different versions, and an upstream publish could break CI and deploys at once with no change here. The version and a SHA-256 checksum are now build arguments, and the tarball comes from the GitHub release assets, which are immutable. Pinned at 7.1.2-27.
Worth knowing: the old archive host was serving a beta-labelled build of that version, so the image had been shipping a beta all along. The release asset is the final build.
-
CI is roughly 3x faster and, for the first time, passes. The workflow had never had a successful run: it ran
go buildon a bare runner with no ImageMagick, which the cgo binding requires. It now builds and tests inside the project image, with the layers cached in the GitHub Actions cache. A cached run takes about 157s.
Fixed
- Per-IP rate limiting could be bypassed entirely. The rate-limit key was built from the raw, unverified
Authorizationheader, so a client sending a different random token on every request minted a fresh counter each time and was never limited. The key is now derived from the verified identity of the credential (<ip>|general,<ip>|bucket:<name>, or the plain<ip>for anything that does not authenticate). This also removes token material from the Redis keyspace.
Upgrade notes
The service will refuse to start if TOKEN is shorter than 32 characters or is a placeholder value. Check it before deploying:
echo -n "$TOKEN" | wc -c # must be >= 32If it is too short, generate a replacement and roll it out to your clients:
openssl rand -hex 32Everything else is backward compatible:
- Deployments with no
config/tokens.jsonbehave exactly as before, using the general token alone. - Token files written before this release stay valid;
expires_atis optional. - Rate-limit counters in flight at deploy time are orphaned by the new key format and expire on their own within the rate-limit window.
Kubernetes users: the manifests now wire TOKEN from cdn-secrets.app_token, which was missing before. Note that MINIO_ROOT_USER, MINIO_ROOT_PASSWORD and the AWS keys are still not wired into the deployment.
Full changelog: v1.8.0...v1.9.0