How to prove immutable backups are actually immutable
Verify immutability by combining storage attributes, identity and policy review, an isolated enforcement test, and a restore result. A refused delete alone proves little: ordinary permissions can cause the same error. Read production metadata without changing it; test deletion only against disposable data in an isolated lab and record the limits of that evidence.
This runbook covers the two targets most Veeam estates actually use: a Linux hardened repository and S3 Object Lock. It is not a tour of every immutable target VBR 13 supports. The closing section lists what it leaves out, and why one of them, NFS and SMB WORM, fails these checks silently.
The executable examples below are read-only. Destructive acceptance testing needs a separate, deployment-specific runbook with exact target validation and disposable data isolated from production. A test prefix or a job name alone does not establish isolation: Veeam objects can contain blocks shared by multiple restore points.
Configured is not verified
The Veeam console shows immutability as a property of a repository. That property records what Veeam was told to do, and what Veeam believes it did. It is not a live read of what the storage enforces right now. Consider a bucket that was never Object-Lock-enabled in the first place. On Amazon S3 you cannot turn it off afterwards, "After you enable Object Lock on a bucket, you can't disable Object Lock or suspend versioning for that bucket", but nothing stops a repository being pointed at a plain bucket, or at an S3-compatible endpoint whose vendor is less strict. Then there is a hardened repository where the immutability service is stopped, and a job whose last session failed before the flag was applied. All of these coexist happily with a green state in the UI.
That last case is documented. Veeam's hardened repository immutability guide (v13, build 13.1.1.18) states that the immutability flag is set on a file only when the backup session completes, and that if a session fails due to an environmental issue the flag is not set. Veeam will retry on the next run, but if the next run creates a new full backup, the files from the failed run are never made immutable automatically. There is a cmdlet, Set-VBRImmutabilityLockExpirationDate, for setting the flag by hand afterwards. Know its limits before you rely on it. It cannot shorten a period: "It is not possible to reduce immutability via PowerShell commands". It is VMware and Hyper-V only, and it does not support restore points from unstructured data source jobs, application backup policies or transaction logs. For those workloads a session that failed before the flag was applied has no documented manual remedy. If that scenario has ever occurred in your environment, you have mutable restore points sitting in an immutable repository, and no console field will tell you.
The failure is also format-dependent, which makes it harder to spot than the paragraph above suggests. Veeam's note: with per-machine backup formats and an incomplete restore point, "the immutability flag will be set only on successfully created backup files"; with single-file backup format it "will not be set on the backup file" at all. Per-machine formats therefore leave a mixed chain (some files locked, some not) that no single console field represents.
Ground rules before you start
-
Separate production inspection from lab testing. Read production metadata with an authorized read-only identity. For enforcement tests, use a dedicated lab repository or bucket and throwaway workloads with no shared production data. Match the relevant build, storage configuration and policy conditions, and record differences. Wait for the backup session to complete and, on a hardened repository, at least one 20-minute
veeamimmureposvccycle. A lab result covers that setup and the tested identity; it does not certify every production restore point.Be clear about what "disposable" does and does not mean. The test data is disposable in the sense that losing it costs you nothing; it is not deletable on demand. Veeam applies the repository's immutability period to VeeamZIP and exported backups and ignores their retention. The documentation is explicit: "For backups with GFS flags, VeeamZIP backups, and exported backup files, Veeam Backup & Replication applies the immutability period of the object storage repository. Retention of these backup files is not considered". So the artefact you are about to create is undeletable for the full repository period, plus Block Generation on object storage. That is the property you are testing, applied to you. Size it accordingly, note that VeeamZIP always writes a full VBK, and do not run this against a repository configured with a multi-year period unless you are content to pay for the test data that long.
-
Define the identity being tested. In the lab, reproduce the relevant permissions of the repository service account or Veeam storage credential without copying production secrets. Record effective roles, session context and privilege. A local Windows administrator's access to stored Veeam credentials is one threat scenario (Veeam KB4349); root access to a repository is a different one. Never interpret a failure caused by missing baseline permissions as evidence of immutability.
-
Distinguish a version delete from a delete marker. On versioned S3, a delete without a version ID can create a delete marker while leaving the protected version intact (S3 Object Lock documentation). Review versions and audit records before diagnosing a successful delete as a broken lock. An isolated enforcement test must identify an exact object version. If an accidental marker affects a production backup, preserve evidence and follow a reviewed recovery procedure; do not improvise another delete.
-
Date and attribute everything. An untimed screenshot is not evidence. Run every command with the system clock visible in the transcript.
Layer 1: Veeam hardened repository (Linux)
First, decide which of the three things v13 calls a hardened repository you are testing, because it changes what is even possible:
- A manually built Linux host. The runbook below works as written, run as the account that owns the backup directory.
- A Veeam Hardened Repository from the Veeam Infrastructure Appliance. SSH is unavailable; the Host Management TUI provides a time-limited root shell after Security Officer approval. Keep host inspection read-only (Checks 1.1 and 1.3). Root and non-owner failures test different privileges, so neither substitutes for a controlled test of the intended identity. Use a separate lab for Check 1.2b; S3 testing applies only if that deployment also uses object storage.
- A Linux backup server from the Veeam Software Appliance, which acts as an immutable repository with no extra preparation. Note what that does to the argument below: the backup server and the repository are the same machine, so "compromising the backup server does not hand over root on the repository" no longer holds.
The paths and bucket names below (/mnt/veeam-repo/, veeam-immutable-repo, Veeam/<test-job>/) are placeholders. Substitute your own; nothing in the checks depends on the layout.
Veeam's hardened repository requires a filesystem that "must support immutable files and extended attributes modified by the chattr and setxattr commands" (requirements and limitations, v13). So there are two things on disk to read: the immutable flag, and the extended attribute carrying the expiry.
Check 1.1: The attributes are actually set
# Read-only: set BACKUP_FILE to an existing absolute backup file path.
: "${BACKUP_FILE:?Set BACKUP_FILE to an absolute file path}"
case "$BACKUP_FILE" in
/*) ;;
*) echo "REFUSING: absolute path required" >&2; exit 1 ;;
esac
[ -f "$BACKUP_FILE" ] || { echo "REFUSING: file not found" >&2; exit 1; }
lsattr "$BACKUP_FILE"
getfattr -d -m - "$BACKUP_FILE"
lsattr should show the i (immutable) attribute on .vbk and .vib files inside their immutability window. It will not show it on .vbm metadata files: Veeam states that "metadata files of a backup chain (.VBM) cannot be immutable because they are updated on every job pass." Do not treat that as a finding; treat a missing i on a .vbk inside its window as one.
Also verify the .veeam.N.lock companion file that Veeam creates alongside each backup file to carry the immutability time period.
Sample the right files or you will manufacture a false negative. Three documented cases produce a missing or short i flag that is correct behaviour, not a finding: files in an inactive chain are not extended ("Veeam Backup & Replication does not extend the immutability for inactive chains"); an incomplete restore point leaves per-machine formats partly flagged and single-file format not flagged at all; and a backup copy job gets no immutability unless GFS is enabled on it.
Check 1.2: Interpret an isolated filesystem test
Production inspection ends with the read-only attributes above. Do not paste a chattr -i / rm sequence into a repository shell. A typo can reach production data, and a sufficiently privileged process can clear the flag before deleting the file.
In a dedicated lab, an operator-reviewed runbook must bind the test to the exact host, canonical file path, repository and effective identity, and reject missing or unexpected values. Test flag removal and file deletion as separate cases, rechecking the flag between them. Compare a protected disposable file with an unprotected disposable control under the same relevant directory permissions and identity. If the control cannot be deleted, a refusal on the protected file is inconclusive. Preserve before/after attributes, exit status and stderr; Operation not permitted alone does not identify the cause. Do not infer protection from a silent command result.
Check 1.2b: Test the backup-server path separately
A host-level check does not establish what a compromised backup server can do. Inventory the candidate backup before designing a separate lab test:
# Read-only inventory; set an exact backup name in the connected lab VBR session.
$backupName = Read-Host 'Exact lab backup name'
if ([string]::IsNullOrWhiteSpace($backupName)) { throw 'Backup name required' }
$backup = @(Get-VBRBackup | Where-Object { $_.Name -eq $backupName })
if ($backup.Count -ne 1) { throw 'Expected exactly one backup' }
Get-VBRRestorePoint -Backup $backup[0]
For VBR 13 build 13.1.1.18, Remove-VBRRestorePoint uses supplied restore points to locate VMs and removes all restore points of the selected VMs or replicas. Selecting one restore point does not limit deletion to that point. -WhatIf previews only that invocation; it does not guard a subsequent invocation without the switch. The cmdlet cannot run when four-eyes authorization is enabled, so that refusal would not prove storage enforcement.
Any destructive test needs isolated lab workloads, verified VM/backup/repository identities and a documented affected set. The console is not an automatic scope safeguard. Record VBR session logs and storage evidence, including a comparable unprotected control. If the operation is blocked before reaching storage, report that control separately and leave storage enforcement unproven.
Check 1.3: The immutability service is running and the clock is trusted
The immutability attributes are maintained by a service, not by the filesystem alone. Veeam documents the split: the Veeam Data Mover Service (veeamtransport, port 6162, running as a non-root user) passes the immutability period to the Veeam Immutability Service (veeamimmureposvc), which "runs with root permissions as a child process of the Veeam Data Mover Service" and checks file immutability attributes every 20 minutes, setting or removing the immutable attribute. Verify the process tree rather than looking for a second systemd unit.
systemctl status veeamtransport
ps -eo user,pid,ppid,comm | grep -E 'veeamtransport|veeamimmureposvc'
# expect veeamimmureposvc running as root with PPID = the veeamtransport PID
ls -la /etc/veeam/immureposvc/ # a retainLock file here means immutability is BLOCKED
cat /etc/veeam/immureposvc/config # may raise the threshold or switch the check off
cat /etc/veeam/immureposvc/timeLog
timedatectl
The timeLog file is Veeam's timeshift detection, written every 10 minutes. It records systemTime, moveTime, hwTime and accelerationTime; if moveTime or accelerationTime exceeds 86,400 seconds (a 24-hour jump), Veeam blocks retention operations and warns in the backup job session. That mechanism exists because rolling the clock forward is the obvious way to expire an immutability window early, and it is worth confirming it is live rather than assuming it.
Two things about that threshold, both of which an audit has to read rather than assume. First, 86,400 is a default, not a constant: KB4482 documents /etc/veeam/immureposvc/config, where maxDeltaValueInSec accepts 60 to 604,800 seconds and disableCheck="1" turns timeshift detection off entirely. A report that certifies a 24-hour threshold while that file says otherwise has certified a control that was weakened or switched off.
Second, the observable state is not the log. When the threshold is crossed, Veeam creates an immutable file named retainLock, and "the presence of the retainLock file blocks the immutable service from changing the immutability state of backup files, both existing and new." That is what to look for, along with the job-session warning repository time shift detected, immutability flag cannot be set. The same thing happens without any attack: shut the repository down for more than 24 hours and retention operations block. Recovery differs by build. On a v13 Veeam Hardened Repository or Software Appliance it is the Reset time shift protection option in the Host Management console TUI; on a manually built host it takes root, chattr -i and rm against retainLock.
Check 1.4: The access path, which is the part that actually protects you
This is the check people skip, and the one that matters. The Linux immutable flag is not magic: the superuser, or any process holding CAP_LINUX_IMMUTABLE, clears it with chattr -i. The hardened repository's model is not that root is powerless. It is that nobody can become root on that machine. So verify the access path:
- Single-use credentials. Veeam's hardened repository documentation describes credentials "used only once to deploy Veeam Data Mover, while adding the Linux server to the backup infrastructure. These credentials are not stored in the backup infrastructure." Confirm the credential does not appear in the Veeam credential manager.
- No SSH, no domain. On a Veeam Hardened Repository deployed from the Veeam Infrastructure Appliance, v13 requires certificate-based authentication and states that SSH cannot be used and the appliance cannot be joined to a domain. A manually built Rocky or Ubuntu host keeps SSH and root unless you remove them, and Veeam's own hardening guidance is that you remove both: the deployment "is done over SSH but ... it is strongly recommended to disable SSH after the initial setup is done", and the root account "should be locked during installation as required by the DISA STIG Profile". So the auditable items on a manual build are
sshddisabled, the service account's temporary sudo rights withdrawn, and root locked. Note what that means for this runbook: on a correctly built repository there is no SSH, so the checks above run from the physical or out-of-band console, which is itself part of the access path you are auditing. - Out-of-band management. Veeam's WORM storage best practice names DRAC, iLO and IMM explicitly: an attacker with out-of-band access "could overwrite the current system." Test reachability of the BMC from a user VLAN. If it answers, your immutability has a side door.
Layer 2: S3 Object Lock
Object Lock has two independent things to verify: the bucket is configured for it, and the individual objects Veeam wrote actually carry retention.
Everything below assumes Amazon S3. On an S3-compatible endpoint add --endpoint-url, and expect the retention mode, the maximum period and the bypass-permission model to differ. Veeam publishes the Compliance-mode guarantee for Amazon S3 only, Backblaze B2 caps Object Lock at 3,000 days and gates bypass on application key capabilities rather than IAM, and Cloudflare R2 implements neither GetObjectLockConfiguration nor GetObjectRetention, so it cannot back a Veeam immutable repository at all.
Check 2.1: Bucket level
aws s3api get-object-lock-configuration --bucket veeam-immutable-repo
aws s3api get-bucket-versioning --bucket veeam-immutable-repo
get-object-lock-configuration returns the bucket's default rule, and AWS notes that this rule "will be applied by default to every new object placed in the specified bucket". Two caveats specific to Veeam: Object Lock only works on buckets with versioning enabled (confirm with get-bucket-versioning), and Veeam's own object storage considerations say you must not set a bucket default retention, because "the default retention may result in an unpredictable system behavior and data loss." So on a correctly built Veeam bucket you expect Object Lock enabled with no default rule. An empty rule set here is the right answer, not a gap. What is a gap is an error: "If you make a request for an Object Lock configuration on a bucket that doesn't have S3 Object Lock enabled, Amazon S3 returns an error." Distinguish the two before you write either one down.
Check 2.2: Object level, which is where the truth lives
aws s3api list-object-versions --bucket veeam-immutable-repo \
--prefix "Veeam/<test-job>/" --max-items 5
aws s3api get-object-retention --bucket veeam-immutable-repo \
--key "Veeam/<test-job>/<object>" --version-id "<VersionId>"
get-object-retention returns the object's Mode and RetainUntilDate. It does not return legal hold status. For that you need get-object-legal-hold, or head-object, which gives you retention mode, retain-until date and legal hold in one call. An object protected only by a legal hold returns nothing here, and reading that as "no protection" is a false finding. For Amazon S3, Veeam's documentation for both v12 and v13 states that "Veeam Backup & Replication will use Compliance object lock mode for each uploaded object." If you see GOVERNANCE on a Veeam-written object in an Amazon S3 bucket, something is wrong and it is worth a support case.
Build the candidate list with list-object-versions. For reading retention you can sample widely. Take the newest object, one from the middle of the window, one from a GFS full; they will legitimately differ because of Block Generation (below).
The examples remain read-only. Keep production samples out of any destructive lab procedure; a fresh shell or a test-looking prefix does not prove that an object is disposable.
Check 2.3: An isolated enforcement test with a positive control
Use a dedicated lab bucket and synthetic objects that Veeam never references. A prefix match alone is insufficient: an empty PREFIX turns a shell pattern into a match for every key. For Veeam-chain testing, use a separate lab repository and throwaway workloads; synthetic S3 objects test storage semantics, not Veeam chain recoverability.
The separate test runbook must verify the endpoint, caller account and role, bucket owner, lab bucket, exact key and nonempty version ID against a recorded allowlist before every mutation. Reject placeholders, None/null, unexpected targets and lookup failures. Record retention, legal hold and UTC time before the operation. Do not derive a deletion target from the first result of a production listing.
For an authorized delete-object test, target the exact disposable version. Active Compliance retention should refuse permanent deletion with AccessDenied / HTTP 403. This error alone is inconclusive: missing s3:DeleteObjectVersion, a policy deny or another access control can produce it. Compare with an unprotected synthetic version under the same relevant identity, resource-policy conditions and request context. It must have neither active retention nor legal hold. If that control also fails, investigate permissions rather than reporting a passing immutability test. Check version existence after the test and keep the full result, request ID and timestamp.
Configure S3 CloudTrail data-event logging for the test resources before testing. Object data events are not logged by default and are absent from CloudTrail Event history. Find the DeleteObject event and correlate request ID, identity, bucket, key and version ID; s3:DeleteObjectVersion is the IAM action, not the CloudTrail event name. Query the configured trail or event data store. Missing records are a logging gap, not proof that no request occurred. S3-compatible providers need their own documented audit mechanism.
A Governance bypass test is optional and remains lab-only. It requires the relevant delete permission, s3:BypassGovernanceRetention and the explicit bypass request header; legal hold remains independent. A refusal from a principal without bypass permission does not test whether an authorized principal can bypass. Success on the disposable version is expected Governance behavior, not proof of broken Compliance. Never run bypass deletion against Veeam data blocks: deleting a shared block can invalidate multiple restore points. Review the effective authorization paths in Check 2.4 as well.
Check 2.4: Review who could bypass
Governance bypass requires s3:BypassGovernanceRetention and the explicit bypass header, in addition to permission for the underlying operation. Review deletion, s3:PutObjectRetention and s3:PutObjectLegalHold separately; removing a legal hold does not remove active Compliance retention.
get-account-authorization-details is a paginated inventory of users, groups, roles and policies in the caller's AWS account. It is a starting point, not an exhaustive list of effective bypass principals. Searching for an action string misses policy semantics such as wildcards, NotAction, effects, resources and conditions. Also examine bucket/access-point policies, cross-account principals and role-assumption paths, session policies, permissions boundaries and applicable organization or endpoint controls.
Use the IAM policy simulator to investigate specific action/resource/context combinations. Current documentation supports SCP evaluation, including conditions and resource scoping; that does not remove the need to review applicable SCPs and simulation inputs. Outside the console, supply the resource policy yourself. Resource control policies are unsupported, and endpoint policies, role chaining or multiple resource policies can cause differences from live authorization. Record inputs and unmodeled controls; a simulated result is not live proof.
Report candidate principals, access paths, owners and coverage gaps. An explicit deny can constrain bypass within its effective scope, but its coverage and who can change it also matter. Do not claim that one policy export or deny statement proves that no bypass path exists. Validate any policy change separately before deployment and confirm representative behavior in the isolated lab.
Governance vs Compliance
| Governance mode | Compliance mode | |
|---|---|---|
| Who can delete before expiry | Any principal with s3:BypassGovernanceRetention sending the bypass header | No one, including the AWS account root user |
| Can retention be shortened | Yes, by a bypassing principal | No |
| Can the mode be changed | Yes, by a bypassing principal. AWS says users cannot "alter its lock settings" without special permissions | No |
| Only escape route | Grant or assume the bypass permission | Delete the AWS account |
| Honest description | An operational guardrail | A WORM control |
Legal hold sits outside this table and is worth knowing for exactly that reason: it is independent of both modes, has no expiry date, and survives a bypass. The rule is explicit: "Bypassing governance mode doesn't affect an object version's legal hold status."
AWS states the compliance case plainly: in compliance mode "a protected object version can't be overwritten or deleted by any user, including the root user in your AWS account", and "the only way to delete an object under the compliance mode before its retention date expires is to delete the associated AWS account."
Governance mode is genuinely useful, and AWS suggests it for testing retention settings before committing to compliance. But telling an insurer your backups are immutable while running Governance describes a control an attacker with the right IAM role can switch off. Say which one you run.
What the Veeam UI reports vs what the storage enforces
Even when everything is configured correctly, the numbers will not match, and it is not a fault.
- Block Generation. Veeam adds a Block Generation period on top of the configured immutability period to reduce API calls against the object store. In v13 the default is 30 days for Amazon S3, IBM Cloud, Google Cloud and 11:11 Cloud object storage, and 10 days for all other types. In v12 the same page lists 30 days for Amazon S3 and IBM Cloud only, and 10 days for everything else. So a 30-day minimum immutability period on Amazon S3 yields a
RetainUntilDateof 30 + 30 = 60 days, but only at the start of a block generation. Three things move that number, and all three will show up in your sample. Veeam shortens the generation window as it runs ("The Block Generation period is reduced every week by 7 days"), so a given object sits somewhere between 30 and 60 days out; the guarantee is only the floor, that no block in a generation is immutable for less than the configured period. Since build 13.0.1.1071 the repository default is the job-retention mode rather than the minimum-period mode, which makes actual retentionmax(job retention, minimum period) + Block Generation. A 30-day setting against 45-day job retention gives 75, not 60. And monthly and yearly GFS fulls get no Block Generation at all unless you change the defaults, so the GFS object this runbook tells you to sample will show the bare period. None of those three is a fault. The storage is right; the console field is a setting, not a prediction. - Retention floors and ceilings differ by layer. A hardened repository takes a minimum of 7 days and a maximum of 9999 days; an object storage repository takes a minimum of 1 day and a maximum of 999 days in v13. In v12 the UI capped it at 90. A 3-day policy is legal on one tier and impossible on the other.
- Immutability overrides job retention. Veeam's retention scenarios state that if the job retention period is shorter than the immutability period, backup files are not deleted when job retention expires, only when immutability does. On object storage the same idea appears as actual retention, which is the configured period plus Block Generation. Capacity forecasts built on job retention alone will be wrong.
The evidence pack
Assume the reader of your evidence has no access to your environment and will not take an assertion on trust. A dated, reproducible bundle survives that reading; a summary does not. Assemble:
- A dated command transcript of every check above, with the operator's identity and the host it ran from. Plain text, not screenshots, so it can be diffed against the next quarter's run.
- The isolated test results, including protected and unprotected controls, before/after attributes, exact targets, identities, error codes, request IDs and correlated audit events. Record inconclusive outcomes and differences from production; a failed-delete log alone is insufficient.
- Configuration exports:
get-object-lock-configurationandget-bucket-versioningoutput as JSON; the Veeam repository properties; the IAM policies of every principal that touches the bucket. - The candidate bypass paths from Check 2.4, with owners, reviewed policy scope and unresolved coverage gaps.
- Console screenshots of the repository immutability settings and of restore point properties. These are the weakest artefact and the one most often supplied alone. Include them, but never as the whole pack.
- A restore test result from within the immutability window, showing the locked data is also usable data.
- A change record proving the configuration has not been altered since the test. Without this, item 1 expires the moment you file it.
What this runbook does not prove
- It proves a moment, not a period. Every check above is a point-in-time sample. Someone with the rights to change a bucket policy or stop
veeamimmureposvccan do so the hour after you file the report. Continuous assurance requires monitoring for configuration change, not a quarterly test. - Governance mode remains bypassable by design. No test result changes that. The mitigation is Compliance mode or a hard boundary on who holds the bypass permission, not a better test.
- Root on a manually built hardened repository can clear the immutable flag.
chattr -iworks for a sufficiently privileged local process. What the hardened repository buys you is that obtaining that privilege is hard, and that the backup server's compromise does not hand it over. That is a real and valuable property, and it is not the same property as "mathematically undeletable". - Veeam does not publish the extended-attribute key. The documentation confirms
chattrandsetxattrare used and that an xattr carries the immutability period, but names no key. Dump the attributes withgetfattr -d -m -and read what is there, rather than grepping for a name you assumed. - Compliance mode is not free. Undeletable is also unshrinkable: a mis-set 9999-day retention on a hardened repository is a storage bill you cannot cancel, and on Amazon S3 in Compliance mode the only documented escape is closing the account.
- It covers two targets out of many. VBR 13 also writes immutability to Azure version-level WORM, Veeam Data Cloud Vault, the archive tier, NFS and SMB repositories on WORM-capable storage, Dell Data Domain, HPE StoreOnce, configuration backups, Agent backups and WORM tape. One of those deserves naming because it fails silently under this runbook: on NFS and SMB, Veeam sets
atimeand the read-only attribute and leaves the storage to honour them, solsattrandgetfattrreport nothing and prove nothing. - SOBR retention depends on the extent type. Veeam's performance-tier immutability algorithm for direct-backup object storage repositories added as performance extents includes a move-policy reduction and, in its archive-tier scenario, a GFS limit tied to the operational restore window. These are not general Linux hardened-repository rules. Identify the extent type and applicable scenario before comparing the configured period with observed retention.
- The chain is not the files. Every check here is about backup files. The
.vbmmetadata is mutable by design, and log backup files in an active chain become mutable when their own period expires. So a chain can pass every check above and still fail an application restore. - A passing immutability test says nothing about the contents. A restore point containing already-encrypted data is perfectly immutable and perfectly useless. Immutability preserves what you gave it.
How BackupSec approaches this
Two of the problems above are structural rather than procedural. The bypass-principal list is only as good as the separation between your production identity plane and your backup storage. ZeroVault is our managed S3-compatible storage for immutable Veeam backups: S3 Object Lock with governance and compliance modes and legal hold, where Veeam by default writes compliance-mode locks that nobody, including our own staff, can shorten or remove before they expire. It runs in its own account with separate credentials and IAM, outside your Active Directory domain, so that domain admin on your network is not also deletion rights on your last copy. It is isolated, not air-gapped: it stays reachable over the internet, so pair it with offline media if you need a true air gap.
There is also a check this runbook does not include and should name. Veeam ships a Security & Compliance Analyzer whose rules cover exactly this ground: Compliance mode on immutable repositories, immutable or air-gapped media, SSH disabled on hardened repositories. Run it before you run anything here; it is free, it is already installed, and it will find in a minute what the commands above take an afternoon to confirm.
The other problem is that a test is a moment. ZeroMON runs on-premise as a read-only client and tracks immutability and hardening status, configuration drift and change detection continuously, so the gap between "it was immutable in March" and "it is immutable today" is an alert rather than an assumption.
ZeroMON reads repository immutability configuration from Veeam and tracks changes. That is configuration monitoring; it does not replace storage-level metadata checks or controlled enforcement and restore tests. Schedule authorized read-only inspection, keep destructive acceptance testing in a separate lab runbook, and use monitoring to detect configuration changes between reviews.
