Schedule icon
List icon
Script icon
If icon
Switch icon
Loop icon
Copy icon
Log icon
SlackIncomingWebhook icon

MinIO Object Policy Compliance Gate

Audit a MinIO bucket for oversized or misplaced objects, alert on breach, and optionally quarantine them by moving into a prefix.

Categories
DataInfrastructure

A bucket with no enforced write policy accumulates risk quietly: a job dumps an unexpectedly large export where a small file was expected, or a client writes to a path nobody approved. Neither failure throws an error - the object just sits there until someone notices, often during an incident. This blueprint lists a MinIO bucket, flags any object that exceeds a configured size limit or whose key falls outside an allowed-prefix allowlist, alerts Slack on breach, and can quarantine the offending objects by moving them under a quarantine prefix - never deleting anything outright.

How it works

  1. periodic_policy_check (io.kestra.plugin.core.trigger.Schedule) runs every 6 hours, always forcing auto_remediate: "false" and dry_run: "true" through its own inputs: override regardless of this flow's defaults. Shipped disabled so you can validate a manual run first.
  2. list_bucket_objects (io.kestra.plugin.minio.List) lists every object under audit_prefix, returning each object's key, size, lastModified, etag, and owner.
  3. evaluate_object_policy (io.kestra.plugin.scripts.python.Script) reads that listing and classifies each key as oversized (size > max_object_size_mb * 1024 * 1024), outside the allowed prefixes (allowed_prefixes), both, or compliant, emitting violation_count, violation_keys, oversized_keys, and unauthorized_location_keys through Kestra.outputs.
  4. evaluate_policy_gate (io.kestra.plugin.core.flow.If) flags a breach whenever violation_count > 0.
    • then (breach): route_remediation (io.kestra.plugin.core.flow.Switch on auto_remediate) either considers remediation ("true", gated again by dry_run through check_dry_run) or only logs that remediation is disabled ("false"). When both gates allow it, quarantine_violating_objects (io.kestra.plugin.core.flow.Loop) moves each violating key into quarantine_prefix with io.kestra.plugin.minio.Copy (delete: true, so the source key is removed once the copy succeeds). alert_policy_violation always fires to Slack regardless of which path ran.
    • else (compliant): log_policy_healthy records a clean check.
  5. log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.
  6. The errors block alerts Slack separately if the flow itself fails - an unreachable MinIO endpoint must not read as "no breach".

What you get

  • A concrete, per-object breakdown of two independent policy checks - size and location - rather than one generic alert.
  • Remediation that only ever moves an object under quarantine_prefix; nothing is deleted outright.
  • Two independent gates (auto_remediate, dry_run) between "violation detected" and "object actually quarantined".
  • A final status log every run, breach or not, so the execution history doubles as a compliance trend for the bucket.

Who it's for

  • Platform and security teams running self-hosted MinIO (or Ceph, SeaweedFS, Garage) who need a write-policy check their object store does not enforce natively.
  • Data engineers who want to catch an oversized or misrouted export before it is read downstream.
  • Teams on air-gapped or on-prem infrastructure who cannot rely on a cloud provider's native bucket-policy console.

Why orchestrate this with Kestra

MinIO's own API surface lists and copies objects but has no policy engine that compares an object's size or key against a rule and decides what to do about it. Kestra fills that gap: List supplies the raw inventory, the script step applies the actual policy, If/Switch make the decision a first-class, auditable branch, Loop isolates each quarantine move so one failure never blocks the rest, and the whole policy lives in version-controlled YAML instead of a convention nobody enforces.

Prerequisites

  • A running MinIO instance (or any S3-compatible object storage that implements the same List/Copy API) reachable at minio_endpoint.
  • A bucket named by bucket with at least one object under audit_prefix.
  • A Slack incoming webhook for notifications.

Local testing: docker run -d --name minio-policy-gate -p 9002:9000 -p 9003:9001 -e "MINIO_ROOT_USER=minioadmin" -e "MINIO_ROOT_PASSWORD=minioadmin" minio/minio server /data --console-address ":9001" docker network connect YOUR_KESTRA_NETWORK minio-policy-gate (only needed if the Kestra Worker runs in a separate Docker network than this container; set minio_endpoint to http://minio-policy-gate:9000 from inside that network, or http://localhost:9002 from the host).

Secrets

  • MINIO_ACCESS_KEY_ID: MinIO access key.
  • MINIO_SECRET_KEY_ID: MinIO secret key.
  • SLACK_WEBHOOK_URL: Slack incoming webhook used by alert_policy_violation and the errors block.
  • In Kestra OSS (no Enterprise secrets backend), secrets are supplied as environment variables prefixed SECRET_, base64-encoded, and read back in flows with {{ secret('NAME') }} - for example SECRET_MINIO_ACCESS_KEY_ID=$(echo -n '<your-minio-access-key>' | base64). This keeps credentials out of the flow YAML but, per Kestra's own documentation, offers no encryption at rest or access control beyond the host environment; use the Enterprise secrets backend for stronger guarantees.

Inputs

  • minio_endpoint (STRING, default http://localhost:9000): MinIO endpoint used by every task.
  • bucket (STRING, default data-lake): bucket audited.
  • audit_prefix (STRING, default empty): restricts the audit to this prefix; empty audits the whole bucket.
  • allowed_prefixes (STRING, default approved/,incoming/): comma-separated prefix allowlist.
  • max_object_size_mb (INT, default 5120): size threshold in megabytes (MB) for the oversized-object check.
  • quarantine_prefix (STRING, default quarantine/): destination prefix for quarantined objects.
  • auto_remediate (SELECT: "false", "true"; default "false"): must be "true" for quarantine to even be considered.
  • dry_run (SELECT: "true", "false"; default "true"): must be explicitly "false", together with auto_remediate: "true", for objects to actually be moved.

Outputs

  • outputs.list_bucket_objects.objects: the raw object listing (key, size, lastModified, etag, owner) for every run.
  • outputs.evaluate_object_policy.vars.violation_count / .violation_keys / .oversized_keys / .unauthorized_location_keys: the computed policy result for every run.

Quick start

  1. Start MinIO locally with the command above, create the bucket named by bucket, and upload a few test objects - some under approved/, some under an unlisted prefix, and one larger than max_object_size_mb.
  2. Add the MINIO_ACCESS_KEY_ID, MINIO_SECRET_KEY_ID, and SLACK_WEBHOOK_URL secrets.
  3. Run the flow manually with the defaults (auto_remediate: "false") and confirm alert_policy_violation fires listing the expected oversized and unauthorized-location counts, and that log_remediation_disabled logs without moving anything.
  4. Run once with auto_remediate: "true" and dry_run: "true" and confirm log_dry_run_remediation describes the quarantine without running it.
  5. Run once with auto_remediate: "true" and dry_run: "false" and confirm the violating objects now live under quarantine_prefix and are gone from their original keys.
  6. Enable periodic_policy_check once you trust the check; it always runs in the safe auto_remediate: "false" / dry_run: "true" mode regardless of what you leave the flow's own defaults set to.

How to extend

  • Extend evaluate_object_policy to also flag unexpected owner.id/owner.displayName values (also returned by List) to catch writes from an access key that should not be uploading to this bucket.
  • Loop over several bucket/audit_prefix pairs with io.kestra.plugin.core.flow.Loop (ForEach is deprecated) to audit multiple buckets in one run.
  • Add a Pause approval task between evaluate_policy_gate and the quarantine branch for a human-reviewed gate instead of (or alongside) the static auto_remediate input.
  • Swap io.kestra.plugin.minio.Copy's delete: true for delete: false to leave a copy in place under quarantine_prefix while keeping the original, if quarantine should not remove the object from its original location.

Pitfalls

  • There is no structured encryption or lifecycle-policy task in this plugin. Verified from the plugin's own documentation: io.kestra.plugin.minio exposes List, Get, Download, Downloads, Upload, Copy, Delete, DeleteList, and CreateBucket - none expose bucket encryption settings or lifecycle rules. This is why the gate audits size and key (fields List actually returns), not encryption-at-rest or lifecycle configuration.
  • Copy's delete property is documented as "Whether to delete the source file after download" - that wording is carried over from the plugin's download-family tasks, but on Copy it is the flag that turns the operation into a move: true removes the source key once the copy to quarantine_prefix succeeds.
  • owner is returned by List but not used by this blueprint's checks. On a single-tenant local MinIO instance with one root access key, every object's owner is identical, so an ownership check only becomes meaningful with multiple access keys; see How to extend.
  • auto_remediate and dry_run are independent gates, not a single boolean. Both must be "true"/"false" respectively for the quarantine Loop to run; setting only one leaves the other still blocking.
  • Switch case keys "true"/"false" are quoted strings. auto_remediate renders as the literal string "true" or "false"; unquoted true:/false: YAML map keys would parse as booleans instead and would not match.

Links

See How

New to Kestra?

Use blueprints to kickstart your first workflows.