Schedule icon
Commands icon
Docker icon
Loop icon
List icon
Log icon
If icon
Pause icon
Fail icon
SlackIncomingWebhook icon

Lock down MinIO buckets left open to anonymous access

Find MinIO buckets exposed by an anonymous download, upload or public policy, then run mc anonymous set none behind an approval and re-verify.

Categories
DataInfrastructure

A MinIO bucket left with an anonymous download, upload or public policy serves its objects to anyone on the internet with no credentials. It is the classic public bucket data exposure. It usually comes from a quick "just make it work" during setup that nobody walks back. This blueprint finds those buckets, records exactly what each one is handing out, then removes the anonymous policy behind an approval and proves the bucket is private again.

This blueprint was created by zkasuran.

Safety

Locking a bucket down removes anonymous access, which breaks any anonymous client currently reading from it. That is the point of the fix, not a side effect, so the destructive step sits behind a Pause: a person approves the exact set of buckets before any policy changes. Read-only is the default (dry_run: true, auto_remediate: false), the allowlist spares buckets that are public on purpose. A ceiling refuses a run that would change more buckets than you expect.

How it works

  1. scan_policies (io.kestra.plugin.scripts.shell.Commands on the mc image) lists every bucket and reads each one's anonymous policy with mc anonymous get. A policy other than private means the bucket is exposed. Exposed buckets that are not on the allowlist are flagged. A connection or credential failure fails the task, so an unreachable endpoint never reads as a clean result.
  2. inventory_exposed (io.kestra.plugin.core.flow.Loop) runs list_objects (io.kestra.plugin.minio.List) on each flagged bucket to record the objects an unauthenticated caller can read right now. This is the blast radius, captured before anything changes.
  3. gate (io.kestra.plugin.core.flow.If) changes a policy only when auto_remediate is true, dry_run is false and at least one bucket is flagged. ceiling_check refuses a run that would exceed max_lockdowns.
  4. confirm (io.kestra.plugin.core.flow.Pause) holds for a human. On approval, remediate_buckets (io.kestra.plugin.core.flow.Loop, concurrencyLimit: 1) runs set_none (mc anonymous set none) on each flagged bucket, then reverify_bucket reads the policy again and assert_locked fails the run if the bucket is still exposed.
  5. report posts a summary to Slack. The errors block posts a separate alert if the scan fails, a lockdown errors or a bucket is still exposed after remediation.

What you get

  • A conservative detector that treats any anonymous policy other than private as exposed, so an unusual or custom grant is caught rather than skipped.
  • An inventory of exactly which objects each exposed bucket is serving, recorded before the fix.
  • A fix that is read-only by default and runs the real mc anonymous set none only after a person approves the exact buckets.
  • A re-read of every policy after the change that fails the run if a bucket is still open, so a lockdown that did not land is never reported green.

Who it's for

  • Platform teams running self-hosted MinIO (or any S3-compatible store that mc speaks to) who need to keep anonymous access off by default.
  • Security engineers auditing object storage for public exposure on a schedule.
  • Teams on on-prem or air-gapped infrastructure with no cloud provider block-public-access control to lean on.

Why orchestrate this with Kestra

Finding a public bucket is one plugin call. Fixing it is another. Proving the fix landed is a third. Kestra ties them into one audited run: the native MinIO plugin inventories the exposure, the mc client reads and sets the policy, the Pause puts a person between detection and a change that breaks live clients. The re-verify step makes the whole thing fail loudly rather than quietly leaving a bucket open. The policy lives in version-controlled YAML, not in a one-off script somebody ran once.

Tested end to end

Run against a MinIO server with four buckets: one anonymous download, one anonymous upload, one private and one download bucket on the allowlist. The scan flags the two exposed non-allowlisted buckets and spares the allowlisted one. A dry run reports them without changing anything. With auto_remediate: true, dry_run: false and an approval, both are set to private, an unauthenticated read that worked before the run is refused after it. The private bucket is never touched. A re-verify against a bucket a concurrent writer keeps re-opening fails the run as designed. An unreachable endpoint routes to the Slack alert.

Prerequisites

Secrets

  • MINIO_ACCESS_KEY_ID: MinIO access key with rights to read and set bucket policies.
  • MINIO_SECRET_KEY_ID: MinIO secret key.
  • SLACK_WEBHOOK_URL: Slack incoming webhook for the summary and the failure alert.

Inputs

  • dry_run (BOOL, default true): report only when true.
  • auto_remediate (BOOL, default false): lock exposed buckets down when true and dry_run is false.
  • minio_endpoint (STRING): the MinIO S3 API endpoint URL.
  • minio_access_key (STRING, default secret('MINIO_ACCESS_KEY_ID')), minio_secret_key (STRING, default secret('MINIO_SECRET_KEY_ID')).
  • allowlist (ARRAY of STRING, default empty): buckets that are public on purpose and are never changed.
  • max_lockdowns (INT, default 25): ceiling on how many buckets one run may change.
  • mc_image (STRING, default minio/mc:latest): the mc client image.
  • docker_network (STRING, default bridge): the Docker network the mc client joins to reach the endpoint. Set it to your MinIO container's network when the endpoint is a container name.

Quick start

  1. Add the three secrets to your Kestra namespace and set minio_endpoint.
  2. Run it. It lists every bucket, flags the exposed ones, inventories them and changes nothing.
  3. Put any bucket that is public on purpose into allowlist and run again. It is reported as spared.
  4. Run with auto_remediate: true and dry_run: false. Approve the pause. The flagged buckets are set to private and the run fails if any is still open.
  5. Set disabled: false on the daily trigger once you are happy with a report-only schedule.

How to extend

  • Point allowlist at a flow input or a namespace KV so the public-by-design set is managed outside the flow.
  • Narrow the scan to a prefix of buckets by filtering the mc ls output, for a very large MinIO.
  • Replace mc anonymous set none with a custom policy via mc anonymous set-json to grant scoped access instead of removing it.
  • Add an object-level check with io.kestra.plugin.minio.List to flag world-readable objects inside an otherwise private bucket.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.