npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
NIST CSF 2.0
MITRE ATLAS
When to Use
- Establishing ransomware-resistant backup infrastructure with cryptographic integrity verification
- Implementing 3-2-1-1-0 backup strategy where the extra 1 is an immutable copy
- Automating backup verification workflows that test restore capability on a schedule
- Protecting backup repositories from deletion or modification by compromised admin accounts
- Meeting compliance requirements for data retention with tamper-proof storage
Do not use as the sole backup solution without also maintaining offline/air-gapped copies. Object lock protects against logical deletion but not physical storage failure.
Prerequisites
- restic binary installed (https://restic.readthedocs.io/)
- S3-compatible storage with Object Lock enabled (AWS S3, MinIO, Backblaze B2)
- Python 3.8+ with subprocess module
- AWS CLI or MinIO client (mc) configured for bucket access
- Sufficient storage for backup repository (typically 2-3x source data with deduplication)
Workflow
Step 1: Initialize Restic Repository with Encryption
Create an encrypted restic repository on S3-compatible storage with object lock enabled. Restic uses AES-256-CTR for encryption with Poly1305-AES for authentication, ensuring backup data is both confidential and tamper-evident.
Step 2: Configure Object Lock Retention
Enable S3 Object Lock in Compliance mode on the backup bucket to prevent any principal (including root) from deleting or modifying objects during the retention period. Set retention to match your backup window requirements (typically 30-90 days).
Step 3: Automate Backup and Verification
Schedule backup operations with post-backup integrity verification using restic check --read-data which downloads and verifies every data blob against its stored checksum. Log results and alert on any integrity failures.
Step 4: Test Restore Procedures
Periodically restore random files from backup snapshots to a temporary location and compare checksums against the original to validate end-to-end backup integrity. Document restore times for RTO planning.
Key Concepts
| Term | Definition |
|---|---|
| Object Lock | S3 feature that prevents object deletion or overwrite for a specified retention period |
| Compliance Mode | Object Lock mode where even the root account cannot delete objects before retention expires |
| Deduplication | Restic stores data in content-addressable chunks, deduplicating across all snapshots |
| 3-2-1-1-0 | 3 copies, 2 media types, 1 offsite, 1 immutable, 0 errors in verification |
Tools & Systems
- restic: Fast, secure, cross-platform backup tool with built-in encryption and deduplication
- resticpy: Python wrapper for restic CLI operations
- AWS S3 Object Lock: WORM storage for tamper-proof backup retention
- MinIO: Self-hosted S3-compatible storage with Object Lock support
Output Format
BACKUP VERIFICATION REPORT
===========================
Repository: s3:s3.amazonaws.com/company-backups-immutable
Snapshots: 45
Total Size: 2.3 TiB (deduplicated from 8.7 TiB)
Last Backup: 2026-03-11T02:00:00Z
Integrity Check: PASSED (all packs verified)
Object Lock: Compliance mode, 90-day retention
Restore Test: PASSED (15 files verified)References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 1
api-reference.md3.0 KB
API Reference: Implementing Immutable Backup with Restic
Core Functions
| Function | Parameters | Returns |
|---|---|---|
init_repository(repo_url, password, s3_config) |
Repository URL, password, optional S3 credentials | Init result dict |
create_backup(repo_url, password, source_paths, tags, exclude) |
Repo, password, paths list, optional tags/excludes | Snapshot summary |
verify_backup_integrity(repo_url, password, read_data) |
Repo, password, whether to verify data blobs | Verification result |
list_snapshots(repo_url, password) |
Repo, password | Snapshot list with metadata |
apply_retention_policy(repo_url, password, keep_*) |
Repo, password, retention counts | Pruning result |
test_restore(repo_url, password, snapshot_id) |
Repo, password, snapshot to restore | Restore test result |
get_repository_stats(repo_url, password) |
Repo, password | Size, dedup ratio, file count |
S3 Object Lock Configuration
| Function | Parameters | Returns |
|---|---|---|
configure_s3_object_lock(bucket, retention_days, mode) |
Bucket name, days, COMPLIANCE or GOVERNANCE | Config result |
Restic Commands Wrapped
| Command | Description |
|---|---|
restic init |
Initialize encrypted repository |
restic backup --json |
Create snapshot with JSON progress |
restic check --read-data |
Full integrity verification |
restic snapshots --json |
List snapshots |
restic forget --prune |
Apply retention and reclaim space |
restic restore |
Extract files from snapshot |
restic stats --json |
Repository size statistics |
CLI Usage
# Initialize repository
python agent.py --repo s3:s3.amazonaws.com/my-backups --action init
# Backup with tags
python agent.py --repo s3:s3.amazonaws.com/my-backups --action backup \
--source /var/lib/postgresql /etc --tags database config
# Full pipeline: backup + verify + retention + restore test
python agent.py --repo s3:s3.amazonaws.com/my-backups --action full-pipeline \
--source /srv/data --keep-daily 7 --keep-weekly 4Retention Policy Defaults
| Parameter | Default | Description |
|---|---|---|
keep-daily |
7 | Keep 7 daily snapshots |
keep-weekly |
4 | Keep 4 weekly snapshots |
keep-monthly |
12 | Keep 12 monthly snapshots |
keep-yearly |
2 | Keep 2 yearly snapshots |
Python Libraries
| Library | Version | Purpose |
|---|---|---|
subprocess |
stdlib | Execute restic CLI commands |
hashlib |
stdlib | SHA-256 verification of restored files |
tempfile |
stdlib | Temporary restore target directories |
json |
stdlib | Parse restic JSON output |
References
- Restic Documentation: https://restic.readthedocs.io/
- AWS S3 Object Lock: https://docs.aws.amazon.com/AmazonS3/latest/userguide/object-lock.html
- resticpy Python Wrapper: https://mtlynch.github.io/resticpy/
- Veeam 3-2-1-1-0 Strategy: https://www.veeam.com/blog/321-backup-rule.html
Scripts 1
agent.py16.8 KB
#!/usr/bin/env python3
"""Immutable backup agent using restic with S3 Object Lock for ransomware-resistant storage.
Automates backup creation, integrity verification, snapshot retention,
and restore testing against S3-compatible storage with WORM protection.
"""
import os
import sys
import json
import hashlib
import logging
import argparse
import subprocess
import tempfile
import shutil
from datetime import datetime, timezone
logging.basicConfig(
level=logging.INFO,
format="%(asctime)s [%(levelname)s] %(message)s",
handlers=[
logging.StreamHandler(),
logging.FileHandler("restic_backup.log"),
],
)
logger = logging.getLogger(__name__)
def run_restic(args, repo=None, password=None, env_extra=None):
"""Execute a restic command and return parsed output."""
cmd = ["restic"]
if repo:
cmd.extend(["--repo", repo])
cmd.extend(args)
env = dict(os.environ)
if password:
env["RESTIC_PASSWORD"] = password
if env_extra:
env.update(env_extra)
result = subprocess.run(
cmd, capture_output=True, text=True, env=env, timeout=3600
)
if result.returncode != 0:
logger.error("restic %s failed: %s", args[0], result.stderr[:500])
return {
"returncode": result.returncode,
"stdout": result.stdout,
"stderr": result.stderr,
}
def init_repository(repo_url, password, s3_config=None):
"""Initialize a new restic repository with encryption."""
env_extra = {}
if s3_config:
env_extra["AWS_ACCESS_KEY_ID"] = s3_config.get("access_key", "")
env_extra["AWS_SECRET_ACCESS_KEY"] = s3_config.get("secret_key", "")
if s3_config.get("endpoint"):
env_extra["AWS_DEFAULT_REGION"] = s3_config.get("region", "us-east-1")
result = run_restic(["init"], repo=repo_url, password=password, env_extra=env_extra)
if result["returncode"] == 0:
logger.info("Repository initialized: %s", repo_url)
elif "already initialized" in result["stderr"]:
logger.info("Repository already exists: %s", repo_url)
result["returncode"] = 0
return result
def configure_s3_object_lock(bucket_name, retention_days=90, mode="COMPLIANCE"):
"""Configure S3 Object Lock on backup bucket via AWS CLI."""
lock_config = {
"ObjectLockEnabled": "Enabled",
"Rule": {
"DefaultRetention": {
"Mode": mode,
"Days": retention_days,
}
}
}
cmd = [
"aws", "s3api", "put-object-lock-configuration",
"--bucket", bucket_name,
"--object-lock-configuration", json.dumps(lock_config),
]
result = subprocess.run(cmd, capture_output=True, text=True, timeout=30)
if result.returncode == 0:
logger.info(
"Object Lock configured: bucket=%s mode=%s retention=%dd",
bucket_name, mode, retention_days
)
else:
logger.error("Object Lock configuration failed: %s", result.stderr[:300])
return {
"bucket": bucket_name,
"mode": mode,
"retention_days": retention_days,
"success": result.returncode == 0,
"error": result.stderr if result.returncode != 0 else None,
}
def create_backup(repo_url, password, source_paths, tags=None, exclude_patterns=None, env_extra=None):
"""Create a new backup snapshot."""
args = ["backup"]
if tags:
for tag in tags:
args.extend(["--tag", tag])
if exclude_patterns:
for pattern in exclude_patterns:
args.extend(["--exclude", pattern])
args.extend(["--json"])
args.extend(source_paths)
result = run_restic(args, repo=repo_url, password=password, env_extra=env_extra)
if result["returncode"] == 0:
for line in result["stdout"].strip().split("\n"):
try:
msg = json.loads(line)
if msg.get("message_type") == "summary":
logger.info(
"Backup complete: %d files, %s added, snapshot %s",
msg.get("files_new", 0) + msg.get("files_changed", 0),
format_bytes(msg.get("data_added", 0)),
msg.get("snapshot_id", "unknown")[:8],
)
return {
"success": True,
"snapshot_id": msg.get("snapshot_id"),
"files_new": msg.get("files_new", 0),
"files_changed": msg.get("files_changed", 0),
"files_unmodified": msg.get("files_unmodified", 0),
"data_added": msg.get("data_added", 0),
"total_files_processed": msg.get("total_files_processed", 0),
"total_bytes_processed": msg.get("total_bytes_processed", 0),
"duration": msg.get("total_duration", 0),
}
except json.JSONDecodeError:
continue
return {"success": False, "error": result["stderr"][:500]}
def verify_backup_integrity(repo_url, password, read_data=True, env_extra=None):
"""Verify repository integrity by checking all data packs."""
args = ["check"]
if read_data:
args.append("--read-data")
args.append("--json")
logger.info("Starting integrity verification (read_data=%s)...", read_data)
result = run_restic(args, repo=repo_url, password=password, env_extra=env_extra)
verification = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"read_data": read_data,
"passed": result["returncode"] == 0,
"output": result["stdout"][:2000],
"errors": [],
}
if result["returncode"] != 0:
for line in result["stderr"].split("\n"):
if "error" in line.lower() or "fatal" in line.lower():
verification["errors"].append(line.strip())
logger.error("Integrity check FAILED: %d errors", len(verification["errors"]))
else:
logger.info("Integrity check PASSED")
return verification
def list_snapshots(repo_url, password, env_extra=None):
"""List all snapshots in the repository."""
result = run_restic(
["snapshots", "--json"], repo=repo_url, password=password, env_extra=env_extra
)
if result["returncode"] == 0 and result["stdout"].strip():
try:
snapshots = json.loads(result["stdout"])
return {
"count": len(snapshots),
"snapshots": [
{
"id": s["short_id"],
"time": s["time"],
"hostname": s.get("hostname", ""),
"tags": s.get("tags", []),
"paths": s.get("paths", []),
}
for s in snapshots
],
}
except json.JSONDecodeError:
pass
return {"count": 0, "snapshots": []}
def apply_retention_policy(repo_url, password, keep_daily=7, keep_weekly=4,
keep_monthly=12, keep_yearly=2, env_extra=None):
"""Apply snapshot retention policy using restic forget."""
args = [
"forget", "--prune",
"--keep-daily", str(keep_daily),
"--keep-weekly", str(keep_weekly),
"--keep-monthly", str(keep_monthly),
"--keep-yearly", str(keep_yearly),
"--json",
]
result = run_restic(args, repo=repo_url, password=password, env_extra=env_extra)
if result["returncode"] == 0:
logger.info(
"Retention policy applied: daily=%d weekly=%d monthly=%d yearly=%d",
keep_daily, keep_weekly, keep_monthly, keep_yearly,
)
return {
"success": result["returncode"] == 0,
"policy": {
"keep_daily": keep_daily,
"keep_weekly": keep_weekly,
"keep_monthly": keep_monthly,
"keep_yearly": keep_yearly,
},
"output": result["stdout"][:1000],
}
def test_restore(repo_url, password, snapshot_id="latest", target_path=None,
sample_count=5, env_extra=None):
"""Test restore by extracting files and verifying checksums."""
if target_path is None:
target_path = tempfile.mkdtemp(prefix="restic_restore_test_")
else:
os.makedirs(target_path, exist_ok=True)
result = run_restic(
["restore", snapshot_id, "--target", target_path],
repo=repo_url, password=password, env_extra=env_extra,
)
if result["returncode"] != 0:
return {"success": False, "error": result["stderr"][:500]}
restored_files = []
for root, dirs, files in os.walk(target_path):
for fname in files:
fpath = os.path.join(root, fname)
try:
stat = os.stat(fpath)
sha256 = hashlib.sha256()
with open(fpath, "rb") as f:
for chunk in iter(lambda: f.read(65536), b""):
sha256.update(chunk)
restored_files.append({
"path": os.path.relpath(fpath, target_path),
"size": stat.st_size,
"sha256": sha256.hexdigest(),
})
except (PermissionError, OSError):
continue
if len(restored_files) >= sample_count:
break
if len(restored_files) >= sample_count:
break
try:
shutil.rmtree(target_path)
except OSError:
pass
logger.info("Restore test: %d files verified from snapshot %s", len(restored_files), snapshot_id)
return {
"success": True,
"snapshot": snapshot_id,
"files_verified": len(restored_files),
"samples": restored_files,
"restore_path": target_path,
}
def get_repository_stats(repo_url, password, env_extra=None):
"""Get repository statistics including size and deduplication ratio."""
result = run_restic(
["stats", "--json", "--mode", "restore-size"],
repo=repo_url, password=password, env_extra=env_extra,
)
stats = {}
if result["returncode"] == 0 and result["stdout"].strip():
try:
stats = json.loads(result["stdout"])
except json.JSONDecodeError:
pass
raw_result = run_restic(
["stats", "--json", "--mode", "raw-data"],
repo=repo_url, password=password, env_extra=env_extra,
)
raw_stats = {}
if raw_result["returncode"] == 0 and raw_result["stdout"].strip():
try:
raw_stats = json.loads(raw_result["stdout"])
except json.JSONDecodeError:
pass
restore_size = stats.get("total_size", 0)
raw_size = raw_stats.get("total_size", 0)
dedup_ratio = round(restore_size / max(raw_size, 1), 2) if raw_size else 0
return {
"restore_size": restore_size,
"restore_size_human": format_bytes(restore_size),
"raw_size": raw_size,
"raw_size_human": format_bytes(raw_size),
"deduplication_ratio": dedup_ratio,
"total_file_count": stats.get("total_file_count", 0),
}
def format_bytes(size):
"""Format byte count to human-readable string."""
for unit in ["B", "KiB", "MiB", "GiB", "TiB"]:
if abs(size) < 1024:
return f"{size:.1f} {unit}"
size /= 1024
return f"{size:.1f} PiB"
def generate_backup_report(repo_url, password, env_extra=None):
"""Generate comprehensive backup status report."""
snapshots = list_snapshots(repo_url, password, env_extra)
stats = get_repository_stats(repo_url, password, env_extra)
verification = verify_backup_integrity(repo_url, password, read_data=False, env_extra=env_extra)
report = {
"report_timestamp": datetime.now(timezone.utc).isoformat(),
"repository": repo_url,
"snapshot_count": snapshots["count"],
"latest_snapshot": snapshots["snapshots"][-1] if snapshots["snapshots"] else None,
"oldest_snapshot": snapshots["snapshots"][0] if snapshots["snapshots"] else None,
"repository_stats": stats,
"integrity_check": {
"passed": verification["passed"],
"errors": verification["errors"],
},
"backup_strategy": {
"encryption": "AES-256-CTR with Poly1305-AES MAC",
"deduplication": "content-defined chunking (CDC)",
"dedup_ratio": stats.get("deduplication_ratio", 0),
},
}
return report
def main():
parser = argparse.ArgumentParser(
description="Immutable Backup Agent with Restic and S3 Object Lock"
)
parser.add_argument("--repo", required=True, help="Restic repository URL (s3:host/bucket)")
parser.add_argument("--password-file", help="File containing repository password")
parser.add_argument("--action", choices=[
"init", "backup", "verify", "snapshots", "retention", "restore-test",
"stats", "report", "configure-lock", "full-pipeline"
], default="report")
parser.add_argument("--source", nargs="+", help="Source paths to back up")
parser.add_argument("--tags", nargs="+", help="Tags for backup snapshot")
parser.add_argument("--exclude", nargs="+", help="Exclude patterns")
parser.add_argument("--bucket", help="S3 bucket name for Object Lock config")
parser.add_argument("--retention-days", type=int, default=90)
parser.add_argument("--keep-daily", type=int, default=7)
parser.add_argument("--keep-weekly", type=int, default=4)
parser.add_argument("--keep-monthly", type=int, default=12)
parser.add_argument("--snapshot", default="latest", help="Snapshot ID for restore test")
parser.add_argument("--output", default="backup_report.json")
args = parser.parse_args()
password = None
if args.password_file and os.path.exists(args.password_file):
with open(args.password_file) as f:
password = f.read().strip()
else:
password = os.getenv("RESTIC_PASSWORD", "")
if args.action == "init":
result = init_repository(args.repo, password)
print(json.dumps(result, indent=2))
elif args.action == "backup":
if not args.source:
print("Error: --source required for backup")
sys.exit(1)
result = create_backup(args.repo, password, args.source, args.tags, args.exclude)
print(json.dumps(result, indent=2, default=str))
elif args.action == "verify":
result = verify_backup_integrity(args.repo, password, read_data=True)
print(json.dumps(result, indent=2))
elif args.action == "snapshots":
result = list_snapshots(args.repo, password)
print(json.dumps(result, indent=2))
elif args.action == "retention":
result = apply_retention_policy(
args.repo, password,
args.keep_daily, args.keep_weekly, args.keep_monthly, 2,
)
print(json.dumps(result, indent=2))
elif args.action == "restore-test":
result = test_restore(args.repo, password, args.snapshot)
print(json.dumps(result, indent=2))
elif args.action == "stats":
result = get_repository_stats(args.repo, password)
print(json.dumps(result, indent=2))
elif args.action == "configure-lock":
if not args.bucket:
print("Error: --bucket required for Object Lock configuration")
sys.exit(1)
result = configure_s3_object_lock(args.bucket, args.retention_days)
print(json.dumps(result, indent=2))
elif args.action == "report":
report = generate_backup_report(args.repo, password)
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
print(json.dumps(report, indent=2, default=str))
elif args.action == "full-pipeline":
if not args.source:
print("Error: --source required for full pipeline")
sys.exit(1)
print("[1/5] Creating backup...")
backup = create_backup(args.repo, password, args.source, args.tags, args.exclude)
print(f" Backup: {'SUCCESS' if backup.get('success') else 'FAILED'}")
print("[2/5] Verifying integrity...")
verify = verify_backup_integrity(args.repo, password, read_data=True)
print(f" Integrity: {'PASSED' if verify['passed'] else 'FAILED'}")
print("[3/5] Applying retention policy...")
retention = apply_retention_policy(
args.repo, password, args.keep_daily, args.keep_weekly, args.keep_monthly, 2
)
print(f" Retention: {'SUCCESS' if retention['success'] else 'FAILED'}")
print("[4/5] Testing restore...")
restore = test_restore(args.repo, password)
print(f" Restore: {'SUCCESS' if restore['success'] else 'FAILED'}")
print("[5/5] Generating report...")
report = generate_backup_report(args.repo, password)
with open(args.output, "w") as f:
json.dump(report, f, indent=2, default=str)
print(f" Report saved to {args.output}")
if __name__ == "__main__":
main()