NubliVaultby Nublify
← Blog

· 8 min read

Cross-cloud bucket backup: the step-by-step and the traps

How to copy a bucket from OCI to AWS without scattering keys across files, with per-bucket permissions, and why the source credential needs write access on restore day.

Copying objects from one bucket to another is trivial. What isn't trivial is doing it across different providers, repeatably, without leaving a static key on disk, without granting tenancy-wide permissions to a task that needs exactly one bucket, and knowing up front how much the bill will grow.

This text describes the path from OCI Object Storage to Amazon S3. The reverse direction changes the names of the primitives, not the reasoning.

Why cross the cloud boundary

The media durability advertised by providers is not the risk that a cross-cloud copy covers. The copy exists to cover what durability doesn't: a compromised administrative credential, a mass deletion performed by your own automation, a suspended account, a policy mistake that erased what the lifecycle rule was supposed to keep. All of that happens inside the account boundary, and replication within the same account inherits that boundary.

The rule is the same one that separates a copy from a mirror: the destination must not obey whoever controls the source. We covered that distinction in backup is not synchronization.

The minimum design

Three pieces: a read identity at the source, a write identity at the destination, and a process running somewhere. The most important decision is where that process runs, because it determines which of the two credentials can be a machine identity and which will inevitably be a static key.

  • Worker at the source (OCI). Read access via instance principal — no secret in a file. The static key then becomes the AWS one, with write permission at the destination.
  • Worker at the destination (AWS). Write access via the instance's IAM role. The static key then becomes the OCI one, with read permission at the source.

There is no option with zero static keys, unless you set up federation between the two providers. There is a choice about which leak you'd rather have. A leaked read key exposes the contents of the source bucket. A leaked write key reaches the destination — and that is exactly why the destination needs Object Lock and an identity that cannot delete, the subject of immutable backup with Object Lock.

Step by step

1. Inventory the source. Object Storage namespace, compartment, region, exact bucket name. Approximate object count and size distribution. If the bucket has versioning, decide whether you copy only the current version or the full history — those are different projects in both effort and cost.

2. Create the read identity, scoped to the bucket. In OCI, policies are written per compartment, but they accept a resource condition. The format is allow group BackupRead to read objects in compartment <name> where target.bucket.name='<bucket>'. Without the where condition, you have just granted read access to every bucket in the compartment to a task that needs one. In an MSP environment that is enough for one customer to reach another customer's data — the topic of how to stop one customer's backup from touching another's.

3. Decide between the native API and S3 compatibility. OCI exposes an S3-compatible API, which lets you use tools that already speak S3 on both ends. The price is that this route depends on a customer secret key — a static key pair tied to a user — and it has implementation differences from the native API. The native SDK accepts instance principals and needs no secret, at the cost of writing your own code to list and read. Check the OCI Object Storage documentation before choosing.

4. Prepare the destination. A dedicated bucket, one per customer. Versioning on. Object Lock enabled — deciding at creation time is the simplest path; enabling it later is possible on a versioned bucket, and the current procedure is in the S3 Object Lock documentation. A bucket policy tying the Principal to the exact ARN of the role or user that writes, and denying s3:DeleteObject, s3:DeleteObjectVersion and, if retention is configured, s3:PutObjectRetention and s3:PutObjectLegalHold for that principal. On a versioned bucket, denying only s3:DeleteObject does not prevent version deletion. Whoever writes does not delete. IAM roles handle the machine-identity part; the bucket policy handles the scoping part.

5. Transfer. In a bucket with millions of small objects, bandwidth is almost never the bottleneck: listing and request count are. Parallelize by prefix, use multipart only above a threshold that makes sense, and keep your own index of what has already been copied — re-reading the entire source every cycle is what turns a cheap backup into an expensive one. The effect of small-file volume on the bill is in the cost of backing up millions of small files.

6. Verify. Comparing sizes is necessary and insufficient. The ETag of an object uploaded via multipart is not the MD5 of the content, so comparing ETags across providers produces false negatives. Compute and store your own per-object checksum in your index, or use the additional checksum fields S3 offers. The Amazon S3 object integrity documentation describes which algorithms can be sent along with the object.

7. Schedule and observe. A cycle with a report of new objects, changed objects and failures. A backup that doesn't report failures is a backup you believe you have.

The traps

Egress is billed at the source. The typical structure involves per-request charges (LIST, GET and PUT count separately), charges for outbound data volume, and storage charges in the chosen class. Cold classes add two clauses that catch people off guard: a billed minimum retention period and an early-deletion fee. I'm not repeating prices here because tables change; open the pricing page for your bucket's exact region and add up the three components before you run the first cycle. What tends to disappear from the bill is covered in what disappears from a cheap storage bill.

The source credential needs write access if you ever restore back. This trap shows up on incident day, not during configuration. You configured read at the source, write at the destination, everything with least privilege — and on incident day you discover nobody can write back into the source bucket. The answer is not to grant write access to the backup identity. It is to create a second identity, for restores, with manage objects on the source bucket and no permission at the destination beyond read, kept disabled during normal operation and enabled through an explicit procedure. Whoever approves the reactivation should not be whoever operates the backup. And the procedure needs to have been rehearsed, otherwise it's a document, not a capability.

Metadata doesn't cross on its own. Storage class, tags, ACLs, pre-authenticated requests, lifecycle rules and bucket configuration are not objects. Copying objects is not a configuration backup — that part lives in infrastructure as code, versioned in a repository.

Weird keys. Zero-byte objects ending in a slash, keys containing #, ?, accented characters or spaces, and prefix depths greater than what the destination filesystem accepts, if you pass through disk. Treat a key as an opaque byte sequence, never as a path.

Cold restore takes time. Writing to an archive class at the destination is what makes the numbers work, and the cost of that is retrieval time. Worth reading Deep Archive restore in practice and why restore is the part nobody before promising an RTO to a customer.

What this article does not prove

There is no performance measurement here. No throughput numbers, no listing times, no monthly cost figures — because price tables and API limits change by region and by account, and a wrong number in a technical text is worse than no number at all.

And the advice does not apply to everyone:

  • A single small bucket. If it's a few gigabytes in one bucket, rclone on a cron job with a well-guarded key is usually enough. The architecture described above only pays for itself when there are many customers, many buckets and a need to prove scoping.
  • The threat is infrastructure failure, not human action. In that case, replication within the same cloud is simpler, cheaper and faster to restore. Crossing providers is coverage against losing control of the account — if that risk isn't in your threat model, you're paying egress for nothing.
  • Short RTO. A cross-cloud copy, especially in a cold class, is worse in recovery time than regional replication. Anyone who needs minutes needs both things, not one.
  • Small team. Two identities per customer, static key rotation and a break-glass procedure are continuous operational cost. Poorly maintained automation here causes more incidents than it prevents.

Checklist

  • Read permission with a condition on the bucket name, never on the whole compartment.
  • Machine identity on the end where the worker runs; static key only on the other side, with scheduled rotation and vault storage.
  • Dedicated destination bucket, versioned, with Object Lock enabled — at creation is the simplest path; on a versioned bucket you can enable it later, checking the current procedure in the S3 documentation.
  • Write identity with no permission to delete objects or versions, enforced by bucket policy.
  • Your own index with checksums, without relying on ETags across providers.
  • A separate restore identity, with write access at the source, disabled by default.
  • Bucket configuration versioned in IaC, separate from the objects.
  • A timed restore performed at least once, using the real identities.

NubliVault is Nublify's backup product: it packs millions of small files and stores them in a cloud different from the source. How it works · Security