The classic 3-2-1 backup rule calls for three copies of data, on two storage media, with one copy off site. The working data counts as one copy, leaving two backup copies. The rule is a starting point for separating failure exposure. It does not promise that every set of three filenames survives hardware failure, accidental deletion, credential compromise, or a mistake propagated to all copies.
When it breaks
Copies can share more than their contents. Two directories may occupy one disk; two virtual volumes may depend on one storage array; several remote backups may all be deletable by the compromised production administrator. Device, location, and access boundaries need separate examination. An off-site location reduces exposure to a local disaster, but does not automatically make its data offline or immutable.
A hard link is an especially direct counterexample to counting names. It adds another directory entry for the same underlying file. Modifying that file in place changes what both names expose. A separate copied file preserves earlier bytes against that particular mutation, but it can still share the same device and administrative failure risks.
Synchronization is another trap. A mirror that promptly propagates deletion may remove the only version needed for recovery. Retention and version selection matter as much as destination count. Checksums detect disagreement with a trusted expected value, but a checksum stored under the same compromised authority is not by itself proof against malicious replacement.
Check it quickly
Save this as copies_lab.py and run python3 copies_lab.py on a filesystem supporting hard links, with assertions enabled. It uses only temporary files and Python’s standard library. The two directory names simulate locations inside one local filesystem. This is a mutation and restore drill, not evidence that the machine actually has geographically separated backup media.
import hashlib
import json
import os
import shutil
import tempfile
from pathlib import Path
def digest(path):
return hashlib.sha256(path.read_bytes()).hexdigest()
with tempfile.TemporaryDirectory(prefix="xbcatm-copies-") as directory:
root = Path(directory)
primary_site = root / "site-a"
other_site = root / "site-b-simulation"
primary_site.mkdir()
other_site.mkdir()
primary = primary_site / "account.json"
primary.write_text('{"account":"example","balance_cents":2500}\n')
expected = digest(primary)
alias = primary_site / "hardlink.json"
os.link(primary, alias)
independent = other_site / "snapshot.json"
shutil.copy2(primary, independent)
assert os.path.samefile(primary, alias)
assert not os.path.samefile(primary, independent)
primary.write_text('{"account":"example","balance_cents":0}\n')
assert digest(alias) != expected
assert digest(independent) == expected
print("hardlink_changed_with_primary")
print("independent_copy_kept_original_bytes")
primary_site.rename(root / "site-a-unavailable")
assert not primary.exists()
restored = root / "restored.json"
shutil.copy2(independent, restored)
assert digest(restored) == expected
assert json.loads(restored.read_text()) == {"account": "example", "balance_cents": 2500}
print("restored_without_original_path", 2500)
assert independent.stat().st_dev == restored.stat().st_dev
print("same_filesystem_lab_not_real_offsite_protection")
The fixture starts with an account containing twenty-five hundred cents. It creates a hard-linked name and a separately copied snapshot, recording the expected digest before changing the original. The identity checks establish which path is the same file. An in-place write changes both the original and hard link to zero, while the independent copy retains the expected bytes.
The first two output markers confirm that contrast. This proves why adding a second filename did not provide another recoverable version against the tested mutation. It does not claim that a filesystem snapshot behaves like a hard link; snapshot implementations have their own copy-on-write and retention semantics.
The script then renames the simulated primary-site directory so the original path is unavailable. Restoration reads only the separate snapshot, writes a new file, checks its digest, and parses the business value. The recovered amount must be twenty-five hundred cents. The final marker explicitly confirms the same-filesystem limitation. All fixture files are removed afterward; no real backup retention policy is changed.
Turn the rule into a recovery design
Inventory the working copy and both backups by storage device or service, physical location, deletion authority, retained versions, and restore dependency. Two different media historically helped avoid common device and format failures. Modern deployments still need meaningful independence; merely renaming cloud resources or folders does not establish it. Document which single failure each separation is intended to survive.
Keep an appropriate copy offline or protected against alteration where the threat model requires it. Protect confidentiality with encryption, and arrange recovery access to the required keys without depending entirely on the failed production environment. Verify that an operator can retrieve the chosen version using the credentials and tooling available during the actual incident.
Rehearse restores from each intended recovery source. Record the recovered business state, its age, and the time needed to make it usable. A recent local copy can provide fast recovery while an older remote copy covers a wider failure, but their recovery-point promises differ. The useful outcome of 3-2-1 is a set of independently usable recovery paths with known limits, supported by exercises instead of a filename count.
Further reading
Sources
- Backblaze 3-2-1 backup strategy, the classic copy/media/location rule and modern device interpretation, checked 2026-09-12.
- CISA StopRansomware Guide, offline encrypted backups, recovery dependencies, and regular restoration tests, checked 2026-09-12.