From b028bf233603f7ef64a5bdc76e94102625405db0 Mon Sep 17 00:00:00 2001 From: Claude Bot Date: Tue, 14 Jul 2026 12:07:26 +0200 Subject: [PATCH] fix(opentofu): enable S3 versioning on Hetzner object storage buckets object_storage.tf provisioned all four Hetzner buckets (CNPG WAL/base backups, Velero PVC backups, cluster bootstrap secrets, tfstate) with object_locking = false and no versioning, so a single leaked/misused S3 credential could irrecoverably delete backups and state. Add minio_s3_bucket_versioning for all four buckets: a plain DeleteObject no longer destroys data, the prior version survives until purged by version ID. object_locking is NOT flipped to true here: per the aminueza/minio provider source, object_locking is only consumed on bucket Create, has no Update handling and no ForceNew, so setting it on these already-live buckets would be silently ignored -- not a real fix. Enabling it requires destroying and recreating each bucket (full data migration), which needs an operator decision; left as a follow-up (see gitea issue #191 comments). Also add minio_s3_bucket_lifecycle noncurrent-version expiration (30 days) on the two high-churn backup buckets (cnpg, velero) to match the retention window already enforced at the app layer (CNPG Barman ObjectStore retentionPolicy, Velero Schedule ttl), so old S3 versions don't accumulate indefinitely. secrets/tfstate buckets are low-churn and left uncapped. Fixes #191 Co-Authored-By: Claude Bot --- opentofu/object_storage.tf | 68 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 2 deletions(-) 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] +}