Short-Lived, Per-Project Storage Credentials — Overview

This plan is split into subplans, one per implementation phase. Read this overview first, then implement the subplans in order — each phase depends on the previous one:

  1. short-lived-storage-credentials-01-storage-backend-model.mdStorageBackend model + bootstrap migration
  2. short-lived-storage-credentials-02-hooks-run-first-select-storage.mdhooks.run_first + select_storage
  3. short-lived-storage-credentials-03-credential-strategies.md — Credential strategies (depends on backend-installable-package.md)
  4. short-lived-storage-credentials-04-runner-refresh-loop.md — Runner-side refresh loop

Goal

Decouple a project's storage backend (MinIO / GCS / S3) from the single global setting it is today, and replace the single static, non-expiring credential pair stored on Project with a pluggable per-backend credential strategy that supports short-lived, auto-refreshed credentials.

This plan is independent of multi-cluster execution (multi-cluster-execution.md) and ships value on the existing single-minikube deployment: today, Project.storage_access_key / storage_secret_key are static forever. That is a real security gap regardless of how many clusters ever run jobs. Multi-cluster execution depends on this plan's StorageBackend model (a project's storage must already be resolved independently of where its jobs run), but this plan does not depend on multi-cluster in any direction.

Background — current state

Architecture Summary

Implementation Order

  1. Phase 1 (...01-storage-backend-model.md) — StorageBackend model, Project.storage_backend_id, bootstrap migration. Pure schema + data migration; no behavior change (everything still resolves to the one bootstrap row).
  2. Phase 2 (...02-hooks-run-first-select-storage.md) — hooks.run_first, sorted entry points, select_storage call site. Still no behavior change with zero plugins installed — default always wins.
  3. Phase 3 (...03-credential-strategies.md) — depends on backend-installable-package.md (the backend must already be a pip-installed package registering its own nagelfluh.hooks entry points, so core protocol handlers can register through the fan-out hook instead of a private dict). storage_protocol_handlers hook + registry, MinioProtocolHandler extracted from today's minio_service.py code (refactor, not a behavior change), stub GcsProtocolHandler / S3ProtocolHandler, StaticKeyStrategy / ShortLivedStrategy delegating to the registry. setup_project_storage() dispatches on credential_strategy only; protocol dispatch is fully inside the registry, not duplicated at either call site.
  4. Phase 4 (...04-runner-refresh-loop.md) — ShortLivedStrategy + refresh loop. This is the only phase that changes runtime behavior for real jobs, and only for StorageBackend rows explicitly configured with credential_strategy: short-lived — the bootstrap row stays static-key by default, so nothing changes for existing deployments until an admin opts a backend in.

Open Questions