ransomware defense

Implementing Immutable Backup with Restic

Implements immutable backup strategy using restic with S3-compatible storage and object lock for ransomware-resistant data protection. Automates backup creation, integrity verification via restic check --read-data, snapshot retention policy enforcement, and restore testing. Integrates with AWS S3 Object Lock, MinIO, and Backblaze B2 for WORM (Write Once Read Many) storage that prevents backup deletion or encryption by ransomware actors.

backupimmutableobject-lockransomwarerecoveryrestics3worm
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

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)
Source materials

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 4

Retention 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

Scripts 1

agent.py16.8 KB
Display-only source. This catalog never executes bundled scripts.
#!/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()
Keep exploring