Design proposal: tenant-supplied backup destination and options - #83
Andrey Kolkov (androndo) wants to merge 2 commits into
Conversation
Adds a design proposal for a typed tenant destination and an opaque driver-options blob on Plan/BackupJob, symmetric to RestoreJob.Options. Motivated by the S3 Bucket backup driver (cozystack/cozystack#4235). Signed-off-by: Andrey Kolkov <androndo@gmail.com> Assisted-By: LLM
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Advanced Run ID:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Timofei Larkin (lllamnyp)
left a comment
There was a problem hiding this comment.
Thanks for picking this up. The need is real: a bucket backup that lands in the platform's own object store shares the source's failure domain, and today the API has no way to name anything else. I agree with the main calls here: a typed destination rather than one buried in options, credentials by reference only, and failing the run instead of silently falling back to the class default.
I'd like to propose a different shape for the destination half, though: bring back storageRef instead of an inline destination struct. I also have one hard constraint on the credentials half.
Background
The original API in cozystack/cozystack#1640 had a required storageRef (TypedLocalObjectReference) on Plan, BackupJob and Backup. The Plan copied it onto each BackupJob, the driver recorded it on the Backup, and restore read storage from backup.spec.storageRef. The design doc treated Storage as opaque to core ("drivers read Storage to know how/where to store or read artifacts"), with storage drivers pluggable and their kinds left TBD. cozystack/cozystack#1873 removed it in favour of BackupClass. That was the right call at the time: nothing implemented pluggable storage, so the field was cost without benefit. This proposal is exactly the point where it starts paying for itself.
Proposed shape
// PlanSpec / BackupJobSpec
// StorageRef optionally names where backups are written. When omitted, the
// storage configured by the resolved BackupClass strategy applies.
// +optional
StorageRef *corev1.TypedLocalObjectReference `json:"storageRef,omitempty"`
// BackupSpec / status: the storage actually used, recorded by the driver and
// immutable afterwards. Restore and cleanup resolve storage from here.
StorageRef corev1.TypedLocalObjectReference `json:"storageRef"`The referenced object carries the destination, and its kind decides what that means. Two kinds would cover the motivating cases:
apps.cozystack.io/Bucket, the tenant's own Bucket app. Coordinates and credentials come from the COSIBucketAccessthe platform already provisions, so the tenant supplies no secret at all.- An external S3 storage kind (e.g.
backups.cozystack.io/S3Storage, namespaced) holding endpoint, bucket, region, prefix, an optional CA, and a reference to the credentials (see below).
options can stay as proposed. It's orthogonal.
Why a reference rather than an inline struct
The inline struct puts the S3 schema into the core backups.cozystack.io types. With it, core validates S3 endpoints, buckets and prefixes, and the proposal lists non-S3 destinations as a non-goal because each new backend would mean a core API change.
A typed reference keeps core out of storage semantics, the same way strategyRef (and now BackupClass) keeps it out of backup mechanics. Core only carries the reference from Plan to BackupJob to Backup. Each storage kind owns its own schema and validation. A new backend, whether a different object store or a third-party storage driver, is a new kind and leaves core untouched. That extensibility was the reason storageRef was a reference in #1640, and this proposal is the first time it has a second kind to separate.
Making the destination its own object also helps in practice:
- Validate once, reuse many times. Endpoint policy, credential checks and a reachability probe run against one object with a
Readycondition, not on every Plan and BackupJob, where failures would only show up at run time. - Explicit driver opt-in. Each strategy declares the storage kinds it supports, and admission rejects a Plan whose strategy can't honour the referenced kind. "The driver cannot honor the destination" becomes an admission error, not a failed run.
- Stable anchor for restore and cleanup.
Backup.storageRefpoints at a live object, not coordinates copied into status. A finalizer can block deleting the storage object while Backups still reference it. That gives the retention proposal (community#77), where drivers delete expired artifacts, a well-defined place to look, and a clear failure mode if the credentials have gone. - RBAC on a dedicated resource. "May this tenant send backups off-platform, and to where" becomes RBAC and admission on the storage kind. Admins get that control without editing BackupClasses.
- The default path stays as simple as #1873 made it. No
storageRefmeans the BackupClass storage, as today.
Credentials
Tenants are not going to get write access to TenantSecret. The write-only TenantSecret from an earlier revision of community#74 has been withdrawn, and community#82 is the path for tenant-supplied secrets. Under #82, a tenant keeps the secret in its own secret store, the spec references an entry by name, and a chart-rendered ExternalSecret materialises it for the consumer without the tenant holding any grant on Secrets.
The external S3 storage kind should take its credentials that way: a reference to an entry in the tenant's store, materialised into a Secret the tenant can't read. The "Tenants gain write access to TenantSecret" item and rollout step 2 should come out, and credentialsSecretRef should become a #82-style reference.
Open questions under either shape
These questions are the same for inline destination and storageRef:
- Who makes the connection, and from where. CNPG and the Job-based strategies run in the application namespace, so the tenant's own egress position applies. Velero runs in
cozy-velerowith platform-wide privileges and would need a per-tenant BackupStorageLocation and credential Secret there. An unvetted tenant endpoint reached from that position is an SSRF surface. Error text propagated intostatus.messagewould also give a response channel back to the tenant. Endpoints need an admin-owned policy (allowed schemes, no private or link-local ranges), plus egress limits wherever the writer runs outside the tenant namespace. - Restore from tenant-writable storage. If the tenant can modify artifacts between backup and restore, the restore driver is applying tenant-controlled input. For Velero that means object manifests (the VM strategies include
helmreleasesandsecrets) applied with Velero's privileges, which is more than tenant RBAC allows. I'd keep Velero-based strategies off tenant-writable storage until restore validates what it applies. The per-strategy opt-in above is the natural place to enforce that. - Secrets captured in backups. Velero backups of an app include its labelled Secrets. In tenant-owned storage these become tenant-readable, including any that are deliberately not exposed to the tenant.
Happy to help sketch the storage kinds in more detail if this direction works for you.
|
Thanks — taking this direction. Agreed on the core moves: a On the tampering open-question specifically, the split looks clean. The core API already has a trust anchor Velero lacks: A few things I'd like to settle in this thread before reworking the doc — open under either shape:
Happy to fold the resolved answers into the doc and sketch the two storage kinds' schemas in the same pass. |
…erence Folds the review consensus into the proposal: the destination becomes a storageRef to a typed storage object (apps.cozystack.io/Bucket or a new backups.cozystack.io/S3Storage) rather than an inline S3 struct in the core types, keeping core out of storage semantics and giving restore and cleanup a live anchor. Credentials move to the community#82 model instead of a tenant write grant on TenantSecret. Records the settled calls in Decisions — reference-over-inline, #82 credentials, per-strategy integrity opt-in with single-artifact checksum verification and Velero kept off tenant-writable storage, and failure-domain separation rather than a platform durability SLA — and narrows Open questions to storage-object mutability, the durability story the default relies on, the per-strategy declaration mechanism, enforcement, and off-platform privileged-apply as a separate track. Assisted-by: LLM Signed-off-by: Andrey Kolkov <androndo@gmail.com>
|
Reworked the proposal along these lines and pushed (204c193):
Those are folded into a new
Happy to sketch the two storage kinds' schemas next if the shape looks right. |
Overview
Adds a design proposal (
design-proposals/tenant-backup-destination/) for two tenant-writable additions to the backup API, symmetric to what the restore side already has:destinationonPlan/BackupJob(a target the tenant owns; credentials referenced through aTenantSecret, never inlined);optionsblob (*runtime.RawExtension, symmetric toRestoreJobSpec.Options) for driver-specific backup scope (topics/tables/prefixes) and mode (incremental).Core does not interpret
options; it validatesdestinationstructurally and passes both through to the strategy driver.Why
The backup API is admin chooses the configuration, tenant picks a class — a tenant cannot name where a backup goes or what it contains. This is the gap surfaced by the S3 Bucket backup driver review in cozystack/cozystack#4235: a same-store bucket copy provides no durability, and the destination that makes it meaningful is one the tenant owns off-platform, which the API cannot express today.
Status: Draft — feedback on the two open questions (validation mechanism for the
TenantSecretrule; shared per-driver options schema) especially welcome.