Skip to content

v1.8.0

Choose a tag to compare

@mstgnz mstgnz released this 25 Jul 20:05
· 20 commits to main since this release

Bucket-scoped write tokens, so a credential can be limited to a single bucket instead of the whole service.

Note: v1.6.61 was the previous tag, so this release also carries the untagged 1.7.0, 1.7.1 and 1.7.2 work. Read the upgrade notes at the bottom before deploying: those releases contain breaking changes.

Bucket-scoped write tokens

A token can now authorize writes to exactly one bucket. Wire format is Authorization: Bearer <bucket>:<token>.

  • Accepted on /upload, /batch/upload, /upload-url, /batch/delete, DELETE /{bucket}/{path} and /resize. The bucket form/body field becomes optional for these callers because the token's bucket is authoritative; naming a different bucket returns 403.
  • Rejected on the operator endpoints (/aws/*, /minio/*, /monitor, /metrics, /ws), which act on arbitrary buckets or expose service-wide data and still require the general TOKEN.
  • Configured in config/tokens.json (template: config/tokens.template.json, path overridable via TOKENS_FILE) and read once at boot. The file is git-ignored and docker-ignored, mounted read-only at runtime, and never baked into an image layer.
  • Secrets are SHA-256 hashed in memory after load and compared in constant time. The bucket prefix is attacker-controlled and grants nothing on its own: the secret is only ever compared against the entry for that exact bucket.
  • A missing token file is not an error. Deployments that use only the general TOKEN behave exactly as before. A file that exists but is invalid stops boot rather than silently serving with missing credentials.

Read isolation is out of scope: GET /{bucket}/{path} stays public, so this covers writes only.

Security

  • Rate-limit bypass fixed. The rate-limit key was built from the raw, unverified Authorization header, so a client sending a different random token on every request minted a fresh counter each time and was never rate 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.
  • Operator endpoints separated. AuthMiddleware was split into a general-token-only gate and a bucket-aware one. Without this a bucket-scoped token could have called DELETE /minio/{bucket}/delete on any bucket or started cost-bearing Glacier retrievals.
  • Bucket names validated on creation. Upload paths that implicitly create a missing bucket now require 3-63 characters of lowercase letters, digits or -. Previously any string could create an arbitrarily named bucket. Existing buckets are not re-validated.

Also included since v1.6.61

From 1.7.2:

  • ImageMagick RCE surface hardened. POST /resize was unauthenticated and fed request bytes straight into ImageMagick; it is now auth-gated and runs the same magic-number content check as the upload endpoints. A strict policy.xml disables the Ghostscript delegate chain (PS/EPS/PDF/XPS), the ImageTragick coder class, indirect file reads and all delegates. Raster decode and SVG upload/serving are unaffected.

From 1.7.1:

  • Upload to a non-existent bucket now creates it instead of failing.
  • DELETE /batch/delete reaches BatchDelete again; it was shadowed by the DELETE /{bucket}/* wildcard.

From 1.7.0:

  • Auth bypass fixed: boot fails fast when TOKEN is empty, and token comparison is constant-time. Previously an empty server token let an empty client token authenticate.
  • SSRF fixed: /upload-url rejects non-http(s) schemes and private/loopback/link-local (including cloud-metadata) targets, validated at request time and at dial time, so DNS rebinding does not get through. Opt out with UPLOAD_URL_ALLOW_PRIVATE=true.
  • Path traversal fixed in the Glacier async local-download target.
  • Resize DoS hardened: on-the-fly resize dimensions are clamped to MAX_RESIZE_DIMENSION (default 4096), closing a uint-wraparound giant-allocation path on the public GET route.
  • MIME hardening: X-Content-Type-Options: nosniff on all responses and a sandbox CSP on served SVG objects.
  • /metrics now requires a Bearer token and /ws requires a token query parameter; both were previously open.
  • Uploads with an image extension must actually be valid images. Opt-in optimize=true stores a visually-lossless, size-reduced version. Process-wide ImageMagick resource limits bound decode cost against decompression bombs.
  • Global panic recovery, a rate limiter that fails open instead of panicking on a Redis outage, batch size caps, a bounded header buffer, and .. rejection in object keys.

Upgrade notes

Coming from v1.8.0's own changes, nothing to do. With no config/tokens.json the service behaves exactly as before, using the general TOKEN alone. 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.

Coming from v1.6.61, three things can break an existing deployment:

  1. TOKEN must be set. The service now refuses to boot with an empty token.
  2. /metrics and /ws now require authentication. Prometheus scrapers need the token as a Bearer header; WebSocket clients need ?token=.
  3. MINIO_USE_SSL is now honored. If your .env sets it, confirm the value matches your MinIO deployment.

To start using bucket-scoped tokens:

cp config/tokens.template.json config/tokens.json
openssl rand -hex 32          # one token per bucket, 32 characters minimum
$EDITOR config/tokens.json
docker compose up -d --build  # the file is read once at boot

Full changelog: v1.6.61...v1.8.0