diff --git a/opentofu/object_storage.tf b/opentofu/object_storage.tf index a4aacd6e..f73e8405 100644 --- a/opentofu/object_storage.tf +++ b/opentofu/object_storage.tf @@ -48,7 +48,71 @@ locals { resource "minio_s3_bucket" "k8s" { for_each = var.enable_hetzner_buckets ? toset(local.hetzner_buckets) : toset([]) - bucket = each.value - acl = "private" + bucket = each.value + acl = "private" + + # object_locking can only be set at bucket-creation time (S3/MinIO + # semantics) — the provider silently ignores changes to this field on an + # existing bucket (no ForceNew, no Update handling: see + # aminueza/minio resource_minio_s3_bucket.go, Create-only consumption of + # ObjectLocking). These four buckets are already live (created with + # object_locking = false), so flipping this to true here would be a + # silent no-op, NOT a fix. Enabling object-lock requires destroying and + # recreating each bucket (full data migration) — tracked separately, + # see gitea issue #191 discussion. Do not set this to true without also + # planning the recreation. object_locking = false } + +# Versioning: safe to enable on an already-existing bucket (unlike +# object_locking, this is a real, live S3/MinIO configuration knob, not a +# create-time-only attribute). This is the recoverability fix for issue +# #191: a leaked/misused S3 credential can no longer permanently destroy +# Velero backups, CNPG WAL archives, cluster secrets, or tfstate with a +# plain DeleteObject — the prior version survives until explicitly purged +# by version ID. +resource "minio_s3_bucket_versioning" "k8s" { + for_each = var.enable_hetzner_buckets ? toset(local.hetzner_buckets) : toset([]) + + bucket = minio_s3_bucket.k8s[each.key].bucket + + versioning_configuration { + status = "Enabled" + } +} + +locals { + # Buckets that receive high-churn, app-managed backups (Barman WAL/base + # backups, Kopia PVC snapshots via Velero) — versioning alone would let + # noncurrent versions accumulate forever as those tools prune and + # re-upload. Cap noncurrent-version retention to match the retention + # window the app layer already enforces, so old S3 versions don't outlive + # the backups that superseded them by more than that window: + # - CNPG ObjectStore retentionPolicy: "30d" (kubernetes/seed/*/objectstore.yaml) + # - Velero Schedule ttl: 720h0m0s = 30d (kubernetes/infrastructure/post-crds/velero-schedules.yaml) + # renner-uno-k8s-secrets and renner-uno-tfstate hold small, low-churn + # objects (kubeconfig/talosconfig, tfstate) with no existing retention + # precedent in this repo — left uncapped (keep every version) since + # storage cost there is negligible. + hetzner_buckets_noncurrent_expiry_days = { + "renner-uno-k8s-cnpg" = 30 + "renner-uno-k8s-velero" = 30 + } +} + +resource "minio_s3_bucket_lifecycle" "k8s_noncurrent_expiry" { + for_each = var.enable_hetzner_buckets ? local.hetzner_buckets_noncurrent_expiry_days : {} + + bucket = minio_s3_bucket.k8s[each.key].bucket + + rule { + id = "expire-noncurrent-versions" + status = "Enabled" + + noncurrent_version_expiration { + noncurrent_days = each.value + } + } + + depends_on = [minio_s3_bucket_versioning.k8s] +}