New to Kestra?
Use blueprints to kickstart your first workflows.
Audit a MinIO bucket for oversized or misplaced objects, alert on breach, and optionally quarantine them by moving into a prefix.
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.
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.list_bucket_objects (io.kestra.plugin.minio.List) lists every object under audit_prefix, returning each object's key, size, lastModified, etag, and owner.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.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.log_audit_complete always runs last, printing the headline numbers regardless of which branch fired.errors block alerts Slack separately if the flow itself fails - an unreachable MinIO endpoint must not read as "no breach".quarantine_prefix; nothing is deleted outright.auto_remediate, dry_run) between "violation detected" and "object actually quarantined".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.
minio_endpoint.bucket with at least one object under audit_prefix.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).
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.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.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.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.bucket, and upload a few test objects - some under approved/, some under an unlisted prefix, and one larger than max_object_size_mb.MINIO_ACCESS_KEY_ID, MINIO_SECRET_KEY_ID, and SLACK_WEBHOOK_URL secrets.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.auto_remediate: "true" and dry_run: "true" and confirm log_dry_run_remediation describes the quarantine without running it.auto_remediate: "true" and dry_run: "false" and confirm the violating objects now live under quarantine_prefix and are gone from their original keys.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.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.bucket/audit_prefix pairs with io.kestra.plugin.core.flow.Loop (ForEach is deprecated) to audit multiple buckets in one run.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.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.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.