Swiss...
Encrypted Backup Infrastructure on Swiss Servers: Zero-Knowledge Disaster Recovery with BorgBackup and Restic
Your production data is encrypted in transit and at rest. But your backups — the complete copy of everything you are protecting — might be sitting on a remote server in plaintext, readable by anyone with disk access. This guide covers deploying zero-knowledge encrypted backup infrastructure using BorgBackup and Restic on Swiss server infrastructure, where the backup server never sees your unencrypted data and Swiss law protects the encrypted archives.
September 21, 2026
by SwissLayer 18 min read
Encrypted Backup Infrastructure on Swiss Servers

You spent weeks hardening your production servers. Full-disk LUKS encryption, TLS everywhere, firewall rules locked down to the port level, SSH keys only, fail2ban watching every authentication attempt. Your live data is a fortress. And then, every night at 02:00, a cron job copies all of it — databases, configuration files, application secrets, customer data — to a backup server where it sits in a plain tarball, readable by anyone with SSH access to the backup machine, any data centre technician with physical access, or any government that issues a subpoena to the hosting provider.

This is the backup blind spot. It is remarkably common, even among operators who know better. The production environment gets all the security attention because it faces the internet. The backup environment is treated as a utility — a thing that needs to work, not a thing that needs to be secure. But your backups contain everything your production servers contain, often in a more convenient format. Database dumps are plaintext SQL. Configuration archives contain API keys and database passwords. Application backups include session tokens and encryption keys. A compromised backup server is, in practice, a compromised production environment — just quieter, because nobody monitors backup servers the way they monitor production.

Zero-knowledge encrypted backups fix this. The encryption happens on the source machine before the data leaves it. The backup server receives and stores ciphertext — encrypted blobs that are computationally indistinguishable from random data without the encryption key. The backup server never sees the key. The hosting provider never sees the key. A government subpoena to the hosting provider yields encrypted archives that are useless without the passphrase that only you possess. This is the architecture we are building in this guide, using two of the best tools available for the job: BorgBackup and Restic.

Why Your Backup Server Is Your Weakest Link

Consider the threat model. Your production Swiss VPS serves HTTPS traffic, accepts SSH only from specific IPs, runs a WAF, and has intrusion detection. An attacker compromising that server needs to bypass multiple layers. Your backup server, on the other hand, often has:

Wider SSH access — because "it's just the backup server" and operators SSH in for maintenance from various locations
Less monitoring — no WAF, no IDS, maybe not even fail2ban, because it does not face the public internet
More sensitive data — every database dump, every config file, every secret from every server in the fleet, going back weeks or months
Longer retention — production servers might keep 30 days of logs; backup servers keep months or years of full system snapshots
Plain storage — tar.gz archives or rsync mirrors with no encryption layer

An attacker who compromises the backup server gets a time machine. They can read last week's database, last month's configuration, last quarter's application code. If you rotated a database password three weeks ago because of a suspected breach, the old password is sitting in last month's config backup on the backup server — and with it, access to any data that was protected by that password before the rotation.

Client-side encryption eliminates this entire class of risk. Even if the backup server is fully compromised — root shell, full disk access — the attacker gets encrypted data. The encryption key never touches the backup server. It exists only on the source machines (which are already hardened) and in your secure key storage (which is separate from both production and backup infrastructure).

BorgBackup vs Restic: An Honest Comparison

Both BorgBackup and Restic are open-source, encrypted, deduplicated backup tools. Both support client-side encryption with authenticated encryption (AES-256 with HMAC or Poly1305). Both do content-defined chunking for deduplication across backups. Both are widely used in production by operators who take backup security seriously.

Where they differ matters for your architecture decisions:

BorgBackup (Python, with C extensions):

• Requires Borg to be installed on both the source and the backup server (server runs in append-only mode)
• Uses SSH as the transport — integrates with your existing SSH key infrastructure
• Supports append-only mode on the server side, preventing a compromised source from deleting old backups
• Compression is built-in and configurable per-archive (lz4, zstd, zlib, lzma)
• Repository format is Borg-specific — you need Borg to read it
• Mature, well-audited, used by major organisations
• Single-threaded by default (Borg 2.0 adds parallelism)
• Lock file mechanism means only one operation per repository at a time

Restic (Go, single binary):

• Backup server needs no special software — Restic writes to any storage backend (SFTP, S3, Backblaze B2, Azure Blob, Google Cloud Storage, local filesystem)
• Multi-threaded by default — significantly faster on multi-core machines with fast storage
• Repository format is well-documented and designed for cloud object storage
• No built-in append-only mode (you protect against deletion at the storage layer — S3 Object Lock, filesystem permissions, or a wrapper like rest-server)
• Compression added in v0.14 (zstd)
• Smaller codebase, fewer moving parts

The practical difference: if your backup target is another Linux server you control, BorgBackup's append-only SSH mode is the most straightforward path to an encrypted, deletion-resistant backup. If your backup target is object storage (S3-compatible, Backblaze B2) or you want a stateless backup server, Restic's backend flexibility is the better fit. Many operators run both — Borg for local server-to-server backups and Restic for off-site cloud archives.

Deploying BorgBackup: Server-to-Server Encrypted Backups

Architecture: your production Swiss dedicated server runs BorgBackup as the client. A separate VPS in a different Swiss data centre runs Borg in append-only server mode. All data is encrypted on the production server before transmission. The backup server stores only ciphertext.

On the backup server — prepare the restricted environment:

# Create a dedicated backup user with restricted shell
useradd -m -s /bin/bash borguser
mkdir -p /home/borguser/.ssh /backup/borg-repos
chown borguser:borguser /home/borguser/.ssh /backup/borg-repos
chmod 700 /home/borguser/.ssh

# Add the source server's SSH public key with forced command
# This restricts the key to ONLY run borg serve — no shell access
cat >> /home/borguser/.ssh/authorized_keys << 'EOF'
command="borg serve --restrict-to-repository /backup/borg-repos/production --append-only",restrict ssh-ed25519 AAAA... production-server-backup-key
EOF

chown borguser:borguser /home/borguser/.ssh/authorized_keys
chmod 600 /home/borguser/.ssh/authorized_keys

The command= prefix in the authorized_keys file is critical. It means that even if someone obtains the backup SSH key, they cannot get a shell on the backup server — the key can only execute borg serve against one specific repository. The --append-only flag means the connected client can create new archives but cannot delete existing ones. Even if your production server is fully compromised and the attacker has the SSH key and the Borg encryption passphrase, they cannot destroy your backup history.

On the production server — initialise and configure Borg:

# Install BorgBackup
apt update && apt install -y borgbackup

# Generate a dedicated SSH key for backups
ssh-keygen -t ed25519 -f /root/.ssh/borg-backup-key -N "" \
    -C "production-server-backup-key"

# Copy the public key to the backup server's authorized_keys
# (done manually above, or use ssh-copy-id with modifications)

# Set the encryption passphrase
# CRITICAL: Store this securely — without it, your backups are unrecoverable
# Options: password manager, encrypted USB drive, printed and stored in safe
export BORG_PASSPHRASE="your-extremely-strong-passphrase-here"

# Or better — use a key file
dd if=/dev/urandom of=/root/.borg-keyfile bs=32 count=1
chmod 600 /root/.borg-keyfile
export BORG_KEY_FILE="/root/.borg-keyfile"

# Initialise the repository with authenticated encryption
# repokey-blake2 = AES-256-CTR + BLAKE2b-256 authentication
borg init --encryption=repokey-blake2 \
    -e SSH_AUTH_SOCK="" \
    --rsh "ssh -i /root/.ssh/borg-backup-key -o BatchMode=yes" \
    borguser@backup-server:/backup/borg-repos/production

# Verify initialisation
borg info \
    --rsh "ssh -i /root/.ssh/borg-backup-key" \
    borguser@backup-server:/backup/borg-repos/production

The repokey-blake2 encryption mode encrypts all data and metadata with AES-256-CTR and authenticates it with BLAKE2b. The encryption key is derived from your passphrase and stored (encrypted) inside the repository. This means you need both the passphrase and access to the repository to decrypt — neither alone is sufficient. BLAKE2b is faster than SHA-256 on modern hardware while providing equivalent security, making it the recommended choice for new repositories.

Now create the backup script:

#!/bin/bash
# /usr/local/bin/borg-backup.sh
# Production server encrypted backup to Swiss backup VPS

set -euo pipefail

# Configuration
export BORG_REPO="borguser@backup-server:/backup/borg-repos/production"
export BORG_PASSCOMMAND="cat /root/.borg-keyfile"
export BORG_RSH="ssh -i /root/.ssh/borg-backup-key -o BatchMode=yes -o ServerAliveInterval=30"
BACKUP_NAME="$(hostname)-$(date +%Y-%m-%d-%H%M%S)"
LOG="/var/log/borg-backup.log"
LOCK="/var/run/borg-backup.lock"

# Prevent concurrent runs
exec 200>"$LOCK"
flock -n 200 || { echo "Backup already running" >> "$LOG"; exit 1; }

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG"; }

log "=== Starting backup: $BACKUP_NAME ==="

# Pre-backup: dump databases
log "Dumping PostgreSQL databases..."
pg_dumpall -U postgres | gzip > /var/backups/postgresql-all.sql.gz

log "Dumping MySQL databases..."
mysqldump --all-databases --single-transaction --routines --triggers \
    | gzip > /var/backups/mysql-all.sql.gz

# Create the backup archive
log "Creating Borg archive..."
borg create \
    --verbose \
    --filter AME \
    --list \
    --stats \
    --show-rc \
    --compression zstd,6 \
    --exclude-caches \
    --exclude '/dev/*' \
    --exclude '/proc/*' \
    --exclude '/sys/*' \
    --exclude '/tmp/*' \
    --exclude '/run/*' \
    --exclude '/mnt/*' \
    --exclude '/media/*' \
    --exclude '/lost+found' \
    --exclude '/var/cache/*' \
    --exclude '/var/tmp/*' \
    --exclude '/var/lib/docker/overlay2/*' \
    --exclude '/var/lib/docker/containers/*/log/*' \
    --exclude '/home/*/.cache/*' \
    --exclude '*.pyc' \
    --exclude '__pycache__' \
    --exclude 'node_modules' \
    --exclude '.npm' \
    "::${BACKUP_NAME}" \
    /etc \
    /home \
    /root \
    /var/www \
    /var/backups \
    /opt \
    /srv \
    /var/lib/postgresql \
    /var/lib/mysql \
    2>> "$LOG"

backup_rc=$?

# Prune old archives — keep a sane retention policy
log "Pruning old archives..."
borg prune \
    --list \
    --glob-archives '{hostname}-*' \
    --show-rc \
    --keep-hourly 6 \
    --keep-daily 14 \
    --keep-weekly 8 \
    --keep-monthly 12 \
    --keep-yearly 2 \
    2>> "$LOG"

prune_rc=$?

# Compact repository — reclaim space from pruned archives
log "Compacting repository..."
borg compact 2>> "$LOG"

compact_rc=$?

# Report
global_rc=$(( backup_rc > prune_rc ? backup_rc : prune_rc ))
global_rc=$(( global_rc > compact_rc ? global_rc : compact_rc ))

if [ $global_rc -eq 0 ]; then
    log "=== Backup completed successfully ==="
elif [ $global_rc -eq 1 ]; then
    log "=== Backup completed with warnings ==="
else
    log "=== BACKUP FAILED (rc=$global_rc) ==="
    # Alert — send notification on failure
    # curl -s -X POST "https://your-alert-webhook" -d "Backup FAILED on $(hostname)"
fi

# Clean up database dumps
rm -f /var/backups/postgresql-all.sql.gz /var/backups/mysql-all.sql.gz

exit $global_rc

Make it executable and schedule it:

chmod +x /usr/local/bin/borg-backup.sh

# Schedule nightly backups at 02:00
cat > /etc/cron.d/borg-backup << 'EOF'
# Encrypted backup to Swiss backup server
0 2 * * * root /usr/local/bin/borg-backup.sh
EOF

# Run a test backup now
/usr/local/bin/borg-backup.sh

# Verify the backup exists and is encrypted
borg list $BORG_REPO
borg info $BORG_REPO::$(borg list --short $BORG_REPO | tail -1)

Deploying Restic: Cloud-Ready Encrypted Backups

Restic takes a different approach. Instead of requiring software on the backup server, it writes to standard storage backends. This means your backup target can be an S3-compatible bucket, an SFTP server, a Backblaze B2 bucket, or a local filesystem. The backup server needs zero Restic-specific configuration — it is just storage.

# Install Restic
apt update && apt install -y restic

# Or install the latest version directly
wget "https://github.com/restic/restic/releases/latest/download/restic_0.17.0_linux_amd64.bz2"
bunzip2 restic_0.17.0_linux_amd64.bz2
mv restic_0.17.0_linux_amd64 /usr/local/bin/restic
chmod +x /usr/local/bin/restic

# Initialise a repository — SFTP backend (to your Swiss backup VPS)
export RESTIC_PASSWORD="your-extremely-strong-passphrase-here"
export RESTIC_REPOSITORY="sftp:backupuser@backup-server:/backup/restic-repos/production"

restic init

# Or initialise with S3-compatible storage
export AWS_ACCESS_KEY_ID="your-access-key"
export AWS_SECRET_ACCESS_KEY="your-secret-key"
export RESTIC_REPOSITORY="s3:https://s3.eu-central-1.amazonaws.com/your-bucket/production"
restic init

Restic's encryption uses AES-256 in counter mode with Poly1305-AES for authentication. Every piece of data — file content, metadata, directory structure, filenames — is encrypted before it leaves the source machine. The repository structure on the backup server consists of encrypted blobs organised by content hash. Even the directory structure and filenames are not visible to anyone with access to the storage backend.

The Restic backup script:

#!/bin/bash
# /usr/local/bin/restic-backup.sh
# Encrypted backup using Restic to Swiss storage

set -euo pipefail

# Configuration — load from secure file
source /etc/restic/env.conf
# Contains: RESTIC_REPOSITORY, RESTIC_PASSWORD_FILE, AWS_* (if S3)

LOG="/var/log/restic-backup.log"
LOCK="/var/run/restic-backup.lock"

exec 200>"$LOCK"
flock -n 200 || { echo "Backup already running" | tee -a "$LOG"; exit 1; }

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG"; }

log "=== Starting Restic backup ==="

# Pre-backup database dumps
log "Dumping databases..."
mkdir -p /var/backups/db
pg_dumpall -U postgres 2>/dev/null | gzip > /var/backups/db/postgresql-all.sql.gz || true
mysqldump --all-databases --single-transaction 2>/dev/null \
    | gzip > /var/backups/db/mysql-all.sql.gz || true

# Run the backup
log "Creating backup snapshot..."
restic backup \
    --verbose \
    --tag production \
    --tag "$(hostname)" \
    --exclude-caches \
    --exclude='/dev' \
    --exclude='/proc' \
    --exclude='/sys' \
    --exclude='/tmp' \
    --exclude='/run' \
    --exclude='/mnt' \
    --exclude='/media' \
    --exclude='/lost+found' \
    --exclude='/var/cache' \
    --exclude='/var/tmp' \
    --exclude='/var/lib/docker/overlay2' \
    --exclude='*.pyc' \
    --exclude='__pycache__' \
    --exclude='node_modules' \
    /etc \
    /home \
    /root \
    /var/www \
    /var/backups/db \
    /opt \
    /srv \
    2>> "$LOG"

backup_rc=$?

# Apply retention policy
log "Applying retention policy..."
restic forget \
    --verbose \
    --tag production \
    --group-by "host,tags" \
    --keep-hourly 6 \
    --keep-daily 14 \
    --keep-weekly 8 \
    --keep-monthly 12 \
    --keep-yearly 2 \
    --prune \
    2>> "$LOG"

prune_rc=$?

# Periodic integrity check (full check weekly, quick check daily)
DAY_OF_WEEK=$(date +%u)
if [ "$DAY_OF_WEEK" -eq 7 ]; then
    log "Running full integrity check (weekly)..."
    restic check --read-data-subset=10% 2>> "$LOG"
else
    log "Running quick integrity check..."
    restic check 2>> "$LOG"
fi
check_rc=$?

# Report
if [ $backup_rc -eq 0 ] && [ $prune_rc -eq 0 ] && [ $check_rc -eq 0 ]; then
    log "=== Backup completed successfully ==="
else
    log "=== BACKUP ISSUE: backup=$backup_rc prune=$prune_rc check=$check_rc ==="
fi

# Cleanup database dumps
rm -rf /var/backups/db

exit $backup_rc

The environment configuration file keeps secrets out of scripts:

# /etc/restic/env.conf — restrict permissions
# chmod 600 /etc/restic/env.conf

# SFTP backend
RESTIC_REPOSITORY="sftp:backupuser@backup-server:/backup/restic-repos/production"
RESTIC_PASSWORD_FILE="/etc/restic/password"

# Or S3 backend
# RESTIC_REPOSITORY="s3:https://s3.eu-central-1.amazonaws.com/bucket/production"
# AWS_ACCESS_KEY_ID="AKIA..."
# AWS_SECRET_ACCESS_KEY="..."

# Create password file
echo "your-extremely-strong-passphrase" > /etc/restic/password
chmod 600 /etc/restic/password

Content-Defined Chunking: Why Deduplication Matters for Encrypted Backups

Both BorgBackup and Restic use content-defined chunking (CDC) for deduplication. Understanding how this works explains why these tools are dramatically more storage-efficient than naive encrypted backup approaches like "encrypt a tarball and upload it."

Traditional backup: you tar your data, encrypt the tarball with GPG, and upload it. Tomorrow, one file changes. You create a new tarball, encrypt it, upload it. The two encrypted tarballs share 99% of the same data, but because encryption is applied to the monolithic tarball, they look completely different at the byte level. You are storing (and transferring) 100% of your data every single day for incremental changes.

CDC-based backup: the tools split your files into variable-size chunks based on content boundaries (using a rolling hash like Buzhash or Rabin fingerprint). Each chunk is hashed, encrypted individually, and stored. When a file changes, only the chunks containing the modified bytes are new — the unchanged chunks already exist in the repository and are not stored again. A 10GB backup where 100MB changed daily stores 10GB + 100MB after two days, not 20GB.

The key insight is that chunking happens before encryption. The tool identifies chunk boundaries based on plaintext content, computes a content hash for deduplication, then encrypts each unique chunk. The backup server sees only encrypted chunks and cannot deduce anything about content similarity — it just stores blobs referenced by encrypted metadata.

# See deduplication in action with Borg
borg create --stats ::backup-1 /var/www
# Output shows: Original size: 8.42 GB, Deduplicated size: 8.42 GB

# Make a small change to one file
echo "updated" >> /var/www/html/index.html

borg create --stats ::backup-2 /var/www
# Output shows: Original size: 8.42 GB, Deduplicated size: 1.24 MB
# Only the changed chunks are stored — 99.98% deduplication

# Same with Restic
restic backup /var/www
restic stats latest --mode raw-data
# Shows actual bytes stored vs. source size

For operators running multiple servers backing up to the same repository, deduplication extends across machines. If server A and server B both run Ubuntu 22.04 with similar packages installed, the shared system files are stored once. Only the unique data per server consumes additional space. On a Swiss VPS fleet with 10 similar servers, this can reduce total backup storage by 60-80%.

Protecting Against Backup Deletion: Append-Only and Immutable Storage

Encrypted backups solve confidentiality. But what about integrity and availability? A ransomware operator who compromises your production server will look for backups to destroy. If the compromised server has the credentials to connect to the backup server and delete archives, your backups die with your production data.

BorgBackup's append-only mode on the server side is the first line of defence:

# On the backup server — the authorized_keys already has --append-only
# This means the client can:
#   ✅ Create new archives
#   ✅ Read existing archives
#   ❌ Delete archives (borg delete fails)
#   ❌ Prune archives (borg prune fails)
#   ❌ Compact repository (borg compact fails)

# To actually prune old backups, you must SSH into the backup server
# directly (with a DIFFERENT key that has full access) and run:
borg prune --keep-daily 14 --keep-weekly 8 --keep-monthly 12 \
    /backup/borg-repos/production

# This separation ensures: even if production is fully compromised,
# the attacker cannot touch backup history

For Restic, immutability is implemented at the storage layer:

# Option 1: rest-server with --append-only
# rest-server is Restic's dedicated HTTP backend with append-only mode
wget "https://github.com/restic/rest-server/releases/latest/download/rest-server_0.13.0_linux_amd64.gz"
gunzip rest-server_0.13.0_linux_amd64.gz
mv rest-server_0.13.0_linux_amd64 /usr/local/bin/rest-server
chmod +x /usr/local/bin/rest-server

# Run with --append-only — clients can add but not delete
rest-server --path /backup/restic-repos --append-only --listen :8000

# Systemd service for rest-server
cat > /etc/systemd/system/rest-server.service << 'EOF'
[Unit]
Description=Restic REST Server (append-only)
After=network.target

[Service]
Type=simple
User=backupuser
ExecStart=/usr/local/bin/rest-server \
    --path /backup/restic-repos \
    --append-only \
    --listen 127.0.0.1:8000 \
    --tls \
    --tls-cert /etc/letsencrypt/live/backup.example.com/fullchain.pem \
    --tls-key /etc/letsencrypt/live/backup.example.com/privkey.pem
Restart=on-failure
NoNewPrivileges=yes
ProtectSystem=strict
ReadWritePaths=/backup/restic-repos

[Install]
WantedBy=multi-user.target
EOF

# Option 2: S3 Object Lock (if using S3-compatible storage)
# Enable Object Lock on bucket creation — cannot be added later
aws s3api create-bucket \
    --bucket my-backup-bucket \
    --object-lock-enabled-for-bucket \
    --region eu-central-1

# Set default retention — 90-day governance mode
aws s3api put-object-lock-configuration \
    --bucket my-backup-bucket \
    --object-lock-configuration '{
        "ObjectLockEnabled": "Enabled",
        "Rule": {
            "DefaultRetention": {
                "Mode": "GOVERNANCE",
                "Days": 90
            }
        }
    }'

# GOVERNANCE mode: can be overridden with special permissions
# COMPLIANCE mode: NOBODY can delete until retention expires — not even root

The two-key architecture is the gold standard: the production server has an SSH key (for Borg) or API credential (for Restic) that can only create new backups. A separate administrative key, stored offline or in a hardware security module, has full access for pruning and restoration. No single compromised credential can both access the encryption key and delete backup history.

Verification: How to Know Your Backups Actually Work

An encrypted backup you have never tested restoring is not a backup. It is a hope. Verification should be automated and regular.

#!/bin/bash
# /usr/local/bin/verify-backup.sh
# Automated backup verification — restore and validate

set -euo pipefail

LOG="/var/log/backup-verify.log"
RESTORE_DIR="/tmp/backup-verify-$$"
FAILURES=0

log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" >> "$LOG"; }

cleanup() { rm -rf "$RESTORE_DIR"; }
trap cleanup EXIT

mkdir -p "$RESTORE_DIR"

log "=== Starting backup verification ==="

# --- BorgBackup verification ---
export BORG_REPO="borguser@backup-server:/backup/borg-repos/production"
export BORG_PASSCOMMAND="cat /root/.borg-keyfile"
export BORG_RSH="ssh -i /root/.ssh/borg-backup-key -o BatchMode=yes"

# Get the latest archive name
LATEST=$(borg list --short --last 1 2>/dev/null)
if [ -z "$LATEST" ]; then
    log "FAIL: No Borg archives found"
    ((FAILURES++))
else
    log "Verifying Borg archive: $LATEST"

    # Check archive integrity (cryptographic verification)
    if borg check --archives-only --last 1 2>> "$LOG"; then
        log "PASS: Borg archive integrity check"
    else
        log "FAIL: Borg archive integrity check"
        ((FAILURES++))
    fi

    # Test-restore a critical file
    borg extract --dry-run "::${LATEST}" etc/nginx/nginx.conf 2>> "$LOG"
    if [ $? -eq 0 ]; then
        log "PASS: Borg test extraction (dry-run)"
    else
        log "FAIL: Borg test extraction"
        ((FAILURES++))
    fi

    # Actually restore and verify a known file
    cd "$RESTORE_DIR"
    borg extract "::${LATEST}" etc/hostname 2>> "$LOG"
    RESTORED_HOSTNAME=$(cat "$RESTORE_DIR/etc/hostname" 2>/dev/null)
    ACTUAL_HOSTNAME=$(hostname)
    if [ "$RESTORED_HOSTNAME" = "$ACTUAL_HOSTNAME" ]; then
        log "PASS: Restored hostname matches ($ACTUAL_HOSTNAME)"
    else
        log "FAIL: Hostname mismatch: restored='$RESTORED_HOSTNAME' actual='$ACTUAL_HOSTNAME'"
        ((FAILURES++))
    fi
fi

# --- Restic verification ---
source /etc/restic/env.conf 2>/dev/null || true

if [ -n "${RESTIC_REPOSITORY:-}" ]; then
    # Check repository integrity
    if restic check 2>> "$LOG"; then
        log "PASS: Restic repository integrity check"
    else
        log "FAIL: Restic repository integrity check"
        ((FAILURES++))
    fi

    # Verify latest snapshot can be listed
    LATEST_SNAP=$(restic snapshots --latest 1 --json 2>/dev/null | \
        python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['id'][:8] if d else '')")
    if [ -n "$LATEST_SNAP" ]; then
        log "PASS: Restic latest snapshot accessible ($LATEST_SNAP)"
    else
        log "FAIL: Cannot access Restic snapshots"
        ((FAILURES++))
    fi
fi

# --- Report ---
log "=== Verification complete: $FAILURES failures ==="

if [ $FAILURES -gt 0 ]; then
    log "ACTION REQUIRED: $FAILURES backup verification checks failed"
    exit 1
fi

exit 0

Schedule weekly verification:

# Run verification every Sunday at 06:00
cat > /etc/cron.d/backup-verify << 'EOF'
0 6 * * 0 root /usr/local/bin/verify-backup.sh
EOF

The dry-run extraction test is fast — it verifies that the archive metadata is intact and the file can be located without actually writing data to disk. The full extraction test for a small known file (like /etc/hostname) verifies the entire pipeline: repository access, decryption, decompression, and data integrity. If both pass, you have high confidence that a full restore will succeed.

Key Management: The Part Everyone Gets Wrong

Your backup encryption is exactly as strong as your key management. A 256-bit AES key protecting terabytes of backups is worthless if the passphrase is backup123, stored in a plaintext file on the same server being backed up, or known to only one person who might get hit by a bus.

Production key management for encrypted backups:

Passphrase strength: Use a randomly generated passphrase of at least 20 characters, or a six-word diceware passphrase. The passphrase derives the encryption key through Argon2 (Borg) or scrypt (Restic) — both are memory-hard key derivation functions resistant to GPU cracking. But they cannot compensate for a weak passphrase.
Passphrase storage: Store the passphrase in at least two independent locations. A hardware password manager (YubiKey with HMAC-SHA1 challenge-response). A printed copy in a physically secure location (bank safe deposit box, fireproof safe). An encrypted digital copy in a separate password manager with a different master password. Never store the passphrase on the same server being backed up — that defeats the purpose if the server is compromised.
Key export: BorgBackup stores the repository encryption key (encrypted with your passphrase) inside the repository. If the repository is corrupted, you lose the key. Export it separately.

# Export BorgBackup repository key
borg key export borguser@backup-server:/backup/borg-repos/production \
    /root/borg-repo-key-export.txt

# Store this file separately from both the repository AND the passphrase
# Anyone with this file + the passphrase can decrypt your backups
# Store securely: encrypted USB drive, hardware security module, or
# printed and stored in a safe

# For Restic, export the repository keys
restic key list
# Backup the entire /keys directory from the repository
# Or add an additional key as a recovery mechanism
restic key add --new-password-file /etc/restic/recovery-password

Key rotation: Change the passphrase periodically. Both Borg and Restic support changing the passphrase without re-encrypting all data (the passphrase wraps the actual encryption key, so only the key wrapper changes).

# BorgBackup — change passphrase
borg key change-passphrase borguser@backup-server:/backup/borg-repos/production

# Restic — add new key and remove old one
restic key add
restic key list    # Note the old key ID
restic key remove [old-key-id]

Bus factor: At least two people in your organisation must have access to the backup passphrase and know the restoration procedure. Document the restoration process. Test it with someone who was not involved in setting up the backups. If only the person who configured the backups can restore them, you have a single point of failure that no amount of encryption fixes.

Monitoring Backup Health Without Logging Content

You need to know that backups are running, succeeding, and producing valid archives — without logging the content of what is being backed up. The monitoring should answer three questions: did the backup run? Did it succeed? Can we restore from it?

#!/bin/bash
# /usr/local/bin/backup-monitor.sh
# Lightweight backup health monitoring

set -euo pipefail

ALERT_WEBHOOK="${ALERT_WEBHOOK:-}"
HOSTNAME=$(hostname)
NOW=$(date +%s)
MAX_AGE_HOURS=26  # Alert if last backup is older than 26 hours

# --- Check BorgBackup ---
export BORG_REPO="borguser@backup-server:/backup/borg-repos/production"
export BORG_PASSCOMMAND="cat /root/.borg-keyfile"
export BORG_RSH="ssh -i /root/.ssh/borg-backup-key -o BatchMode=yes"

BORG_LAST=$(borg list --short --last 1 2>/dev/null)
if [ -n "$BORG_LAST" ]; then
    # Get the timestamp of the last archive
    BORG_TIME=$(borg info "::${BORG_LAST}" --json 2>/dev/null | \
        python3 -c "import sys,json; print(json.load(sys.stdin)['archives'][0]['start'])" 2>/dev/null)
    BORG_EPOCH=$(date -d "$BORG_TIME" +%s 2>/dev/null || echo 0)
    AGE_HOURS=$(( (NOW - BORG_EPOCH) / 3600 ))

    if [ $AGE_HOURS -gt $MAX_AGE_HOURS ]; then
        echo "ALERT: Borg backup is ${AGE_HOURS}h old (max: ${MAX_AGE_HOURS}h)"
    else
        echo "OK: Borg last backup ${AGE_HOURS}h ago — ${BORG_LAST}"
    fi

    # Check repository health (quick mode)
    if borg check --repository-only 2>/dev/null; then
        echo "OK: Borg repository integrity verified"
    else
        echo "ALERT: Borg repository integrity check FAILED"
    fi
else
    echo "ALERT: No Borg archives found"
fi

# --- Check Restic ---
source /etc/restic/env.conf 2>/dev/null || true
if [ -n "${RESTIC_REPOSITORY:-}" ]; then
    RESTIC_LAST=$(restic snapshots --latest 1 --json 2>/dev/null | \
        python3 -c "import sys,json; d=json.load(sys.stdin); print(d[0]['time'][:19] if d else '')" 2>/dev/null)

    if [ -n "$RESTIC_LAST" ]; then
        RESTIC_EPOCH=$(date -d "$RESTIC_LAST" +%s 2>/dev/null || echo 0)
        AGE_HOURS=$(( (NOW - RESTIC_EPOCH) / 3600 ))

        if [ $AGE_HOURS -gt $MAX_AGE_HOURS ]; then
            echo "ALERT: Restic backup is ${AGE_HOURS}h old (max: ${MAX_AGE_HOURS}h)"
        else
            echo "OK: Restic last snapshot ${AGE_HOURS}h ago"
        fi
    else
        echo "ALERT: No Restic snapshots found"
    fi
fi

# --- Check disk space on backup target ---
BACKUP_USAGE=$(ssh -i /root/.ssh/borg-backup-key borguser@backup-server \
    "df -h /backup --output=pcent | tail -1 | tr -dc '0-9'" 2>/dev/null)
if [ -n "$BACKUP_USAGE" ] && [ "$BACKUP_USAGE" -gt 85 ]; then
    echo "ALERT: Backup storage at ${BACKUP_USAGE}% capacity"
else
    echo "OK: Backup storage at ${BACKUP_USAGE:-unknown}% capacity"
fi

Notice what this script does not do: it does not log filenames, file sizes, database names, or any content-related metadata. It monitors operational health — timing, integrity, storage capacity — without creating a secondary metadata trail that could be used to infer what you are backing up.

Disaster Recovery: The Full Restoration Procedure

Encryption adds one step to disaster recovery: you need the passphrase before you can access anything. This makes the restoration procedure worth documenting explicitly, because under the stress of an actual disaster is the worst time to figure it out.

# === BorgBackup Full Restoration ===

# 1. On the new/rebuilt server, install Borg
apt update && apt install -y borgbackup

# 2. Set up SSH access to the backup server
# (You need the backup SSH key — stored separately from the server)
mkdir -p /root/.ssh
# Restore the SSH key from your secure storage

# 3. Set the passphrase
export BORG_PASSPHRASE="your-passphrase"
export BORG_REPO="borguser@backup-server:/backup/borg-repos/production"
export BORG_RSH="ssh -i /root/.ssh/borg-backup-key"

# 4. List available archives
borg list

# 5. Restore to a temporary directory first (safer than overwriting /)
mkdir /restore
cd /restore
borg extract "::archive-name-here"

# 6. Selectively restore what you need
# Restore specific paths:
borg extract "::archive-name" etc/nginx
borg extract "::archive-name" var/www
borg extract "::archive-name" var/backups/postgresql-all.sql.gz

# 7. Move restored files into place
cp -a /restore/etc/nginx/* /etc/nginx/
cp -a /restore/var/www/* /var/www/

# 8. Restore database from dump
gunzip < /restore/var/backups/postgresql-all.sql.gz | psql -U postgres
gunzip < /restore/var/backups/mysql-all.sql.gz | mysql


# === Restic Full Restoration ===

# 1. Install Restic on the new server
apt update && apt install -y restic

# 2. Configure repository access
export RESTIC_REPOSITORY="sftp:backupuser@backup-server:/backup/restic-repos/production"
export RESTIC_PASSWORD="your-passphrase"

# 3. List snapshots
restic snapshots

# 4. Restore latest snapshot to a temporary directory
restic restore latest --target /restore

# 5. Or restore a specific snapshot
restic restore abc123de --target /restore

# 6. Or restore specific paths only
restic restore latest --target /restore --include "/etc/nginx" --include "/var/www"

# 7. Mount a snapshot as a FUSE filesystem (browse before restoring)
mkdir /mnt/backup
restic mount /mnt/backup &
# Now browse /mnt/backup/snapshots/latest/ like a normal filesystem
ls /mnt/backup/snapshots/latest/etc/nginx/
# Copy what you need
cp /mnt/backup/snapshots/latest/etc/nginx/nginx.conf /etc/nginx/
fusermount -u /mnt/backup

Restic's FUSE mount feature is particularly useful during disaster recovery. Instead of extracting the entire backup (which could take hours for large datasets), you mount the snapshot as a read-only filesystem and copy only what you need. This is especially valuable when you are restoring to a server with limited disk space — you do not need enough free space to hold both the running system and a full extraction.

The Swiss Jurisdictional Layer for Backup Infrastructure

Client-side encryption is the technical layer. Swiss jurisdiction is the legal layer. Together, they create a backup architecture where accessing your data requires overcoming both barriers simultaneously.

When your encrypted backups reside on a Swiss dedicated server, the data is protected by the Federal Act on Data Protection (FADP) and the Swiss Federal Constitution's privacy guarantees. A foreign government seeking access to your backup data must go through the Swiss Mutual Legal Assistance Treaty (MLAT) process — a formal diplomatic channel that requires Swiss judicial review. Swiss courts evaluate proportionality: is the request specific? Is it justified? Does it meet the dual-criminality requirement (is the alleged offence also a crime under Swiss law)?

But here is the key point for encrypted backups: even if a court order compels the hosting provider to hand over the data, they can only provide the encrypted blobs. The encryption key never touched the backup server. The hosting provider cannot decrypt the data because they never had the ability to. This is not a defiance of the court order — the provider is fully compliant, handing over everything they have. What they have is ciphertext that is useless without the key.

This is the difference between privacy hosting in Switzerland and hosting in jurisdictions with key disclosure laws. The United Kingdom's Regulation of Investigatory Powers Act 2000 (RIPA Part III) makes it a criminal offence to refuse to disclose encryption keys when ordered by a court — up to five years imprisonment for terrorism-related cases. Australia's Assistance and Access Act 2018 can compel individuals and companies to provide "technical assistance" including decryption. France's Criminal Code Article 434-15-2 criminalises refusal to provide a decryption key. Switzerland has no equivalent law. Swiss authorities can compel you to hand over data you possess, but they cannot compel you to decrypt data or disclose encryption keys.

For operators using Swiss VPS infrastructure for offshore hosting of backup data, this legal framework is as important as the AES-256 encryption protecting the bits on disk. The technical and legal layers reinforce each other: encryption makes the data unreadable without the key, and Swiss law does not provide a mechanism to compel key disclosure.

Network Security for Backup Traffic

Even with client-side encryption, the backup traffic itself should travel over encrypted channels. BorgBackup uses SSH by default — already encrypted. Restic over SFTP also uses SSH. Restic to S3 uses HTTPS. But additional network-level measures harden the backup pipeline:

# Firewall rules: backup server accepts SSH only from known sources
# /etc/nftables.conf on the backup server

table inet filter {
    chain input {
        type filter hook input priority 0; policy drop;

        ct state established,related accept
        iif lo accept

        # SSH from production servers only (WireGuard IPs preferred)
        ip saddr 10.100.0.0/24 tcp dport 22 accept

        # rest-server HTTPS from production servers only
        ip saddr 10.100.0.0/24 tcp dport 8000 accept

        # ICMP for network diagnostics
        icmp type echo-request limit rate 5/second accept
    }
}

# If using WireGuard between production and backup servers,
# ALL backup traffic stays within the encrypted tunnel.
# The backup server's SSH port is never exposed to the public internet.

Running backup traffic over a WireGuard tunnel between your production and backup servers means the backup data has three layers of encryption in transit: the application-level encryption (Borg/Restic client-side encryption), the SSH encryption (transport layer), and the WireGuard encryption (network layer). This is defence in depth — even if one layer has a vulnerability, the other two still protect the data.

Performance Tuning for Large-Scale Backups

Encrypted, deduplicated backups are computationally more expensive than plain rsync. The source server is hashing, chunking, compressing, and encrypting data. On servers with terabytes of data, performance tuning matters:

# BorgBackup performance tuning

# Use zstd compression (faster than zlib, better ratio than lz4)
borg create --compression zstd,3 ...
# Levels 1-3: fast compression, moderate ratio
# Levels 4-6: balanced
# Levels 7-9: maximum compression, slow (use for archival backups)

# Increase chunk cache for large repositories
# /root/.config/borg/
export BORG_FILES_CACHE_SUFFIX=".cache"
# Borg caches file metadata to detect unchanged files without hashing
# Increase if you have millions of small files

# For databases: use streaming dumps instead of dump-then-backup
# This avoids writing the full dump to disk before backing up
pg_dumpall -U postgres | borg create --stdin-name postgresql.sql ::db-backup -

# Restic performance tuning

# Parallel file reading (default: 2, increase for fast storage)
restic backup --read-concurrency 4 /data

# Pack size tuning for object storage
restic backup --pack-size 64 /data
# Larger packs = fewer API calls to S3 = lower cost and higher throughput

# Limit bandwidth during business hours
restic backup --limit-upload 50000 /data  # 50 MB/s cap

# Use --one-file-system to avoid crossing mount boundaries
restic backup --one-file-system /

# Cache directory for faster subsequent operations
export RESTIC_CACHE_DIR="/var/cache/restic"
mkdir -p "$RESTIC_CACHE_DIR"

For servers with NVMe storage and fast network connections to the backup target, the bottleneck is usually CPU (encryption and hashing). For servers with spinning disks, the bottleneck is I/O. For servers backing up to remote targets over WAN, the bottleneck is bandwidth. Identify your bottleneck before tuning — optimising the wrong layer wastes time.

What This Does Not Protect Against

Transparency about limitations:

Compromised source server at backup time: If an attacker has root access to your production server when the backup runs, they can read the data before it is encrypted, or capture the passphrase from the process environment. Client-side encryption protects backups at rest on the backup server — it does not protect the source server itself. That is what server hardening, intrusion detection, and access control are for.
Lost passphrase: If you lose the encryption passphrase and all copies of the exported key, your backups are permanently unrecoverable. This is by design — it is the same property that protects you from adversaries. Key management is not optional; it is the most critical operational component of encrypted backups.
Backup server availability: Encrypted backups on a single server still have a single point of failure for availability. Borg and Restic repositories should be replicated to a second location — either a second Swiss data centre or geographically separate high-bandwidth infrastructure. The replication can be at the storage level (rsync of the encrypted repository) because the data is already encrypted.
Metadata analysis: While the backup content is encrypted, an observer with access to the backup server can see metadata: backup timestamps (revealing your backup schedule), total repository size (revealing approximate data volume), and growth rate. This is less sensitive than content, but it is not zero information. Running backup traffic through WireGuard and using a dedicated backup server (not shared with other tenants) minimises metadata exposure.

Getting Started: Minimum Viable Encrypted Backup

If the full dual-tool, append-only, monitored, verified architecture looks like more than you need right now, here is the minimum to go from "backups in plaintext" to "backups encrypted with zero-knowledge architecture" in about 30 minutes:

1. Install BorgBackup on your production server (apt install borgbackup)
2. Initialise a repository with --encryption=repokey-blake2 — on the same server for now, on a separate partition or drive
3. Run your first backup: borg create --compression zstd,3 ::first-backup /etc /home /var/www
4. Export the repository key: borg key export — save this somewhere safe, not on this server
5. Write down the passphrase and store it in a password manager
6. Add a cron job to run the backup nightly
7. Test restoration: borg extract ::first-backup etc/hostname

That gives you encrypted, deduplicated, compressed backups immediately. Move the repository to a remote Swiss VPS backup server when you are ready. Add append-only mode, monitoring, verification, and Restic for off-site cloud archives as your infrastructure grows.

Your production data deserves backups that are as secure as the production environment itself. Client-side encryption with BorgBackup or Restic, stored on Swiss infrastructure where the legal framework complements the cryptographic protection, is how you build backup infrastructure you can actually trust. Not trust in the sense of "I hope nobody looks at it" — trust in the mathematical sense of "they cannot read it even if they try."