Project: Automated ZFS replication via encrypted removable media
Environment: FreeBSD with ZFS and GELI encryption
Repository: http://svn.dailydata.net/svn/zfs_utils/trunk
License: BSD 2-Clause (FreeBSD License)
The sneakernet project provides a complete automated solution for air gap server replication using ZFS datasets and encrypted transport media. This implementation was developed for a production environment requiring monthly off-site backups with split-key security architecture.
Key Features:
A production deployment required the following specifications:
| Requirement | Implementation |
|---|---|
| Replication Schedule | Monthly updates from in-house backup server to air gap server |
| Transport Media | 3× 1.9TB SSD drives in rotation |
| Drive Rotation | One at source, one at target, one in transit — minimizes site visits |
| Security Model | Multi-layer encryption with split-key architecture |
| Location | Air gap server in unsecured location (mandatory encryption) |
| Automation | Fully automated with maintenance script execution |
Encryption Layers:
GELI full disk encryption on air gap serverGELI key derived from:Split-key advantage: Neither component alone can decrypt the air gap server. Compromise of a single key (server or transport) does not expose data.
GELI kernel module (for target server encryption)
Repository URL: http://svn.dailydata.net/svn/zfs_utils/trunk
Sub-project: sneakernet
Export the project:
mkdir -p /usr/local/opt svn export http://svn.dailydata.net/svn/zfs_utils/trunk /usr/local/opt/zfs_utils cd /usr/local/opt/zfs_utils
The sneakernet script uses YAML configuration files:
sneakernet.conf.yamlsneakernet.datastructureKey Configuration Sections:
source — Source server settings (hostname, poolname)target — Target server settings (hostname, poolname, GELI config)transport — Transport drive settings (label, encryption key, mountpoint)datasets — Dataset replication mappingsExample minimal configuration snippet:
source: hostname: backup-primary poolname: tank target: hostname: airgap-backup poolname: backup transport: label: sneakernet fstype: ufs mountPoint: /mnt/sneakernet encryptionKey: your_hex_key_here datasets: dataset1: source: tank target: backup dataset: data
The source server performs the following operations automatically:
Command:
/usr/local/opt/zfs_utils/sneakernet/sneakernet
The target server performs the following operations automatically:
GELI encrypted disks using combined keyshutdownAfterReplication enabled)Command:
/usr/local/opt/zfs_utils/sneakernet/sneakernet
The script automatically detects whether it's running on the source or target server by comparing the hostname to the configuration. No mode flags required.
The three-drive rotation minimizes operational overhead:
Normal Operation Cycle:
| Month | Drive A | Drive B | Drive C | Action Required |
|---|---|---|---|---|
| 1 | At Source (ready) | At Target | In Transit to Target | Operator: Deliver Drive C to target |
| 2 | At Source (ready) | In Transit to Source | At Target (ready) | Operator: Collect Drive B from target |
| 3 | In Transit to Target | At Source (ready) | At Target | Operator: Deliver Drive A to target |
| 4 | At Target | At Source (ready) | In Transit to Source | Operator: Collect Drive C from target |
Benefits:
Drive Labeling:
Each transport drive should be labeled with GPT labels:
# Label drives for easy identification gpart add -t freebsd-ufs -l sneakernet_A /dev/ada0 gpart add -t freebsd-ufs -l sneakernet_B /dev/ada1 gpart add -t freebsd-ufs -l sneakernet_C /dev/ada2 # Create filesystems newfs -U /dev/gpt/sneakernet_A newfs -U /dev/gpt/sneakernet_B newfs -U /dev/gpt/sneakernet_C
Generate transport key:
# Generate 32-byte (256-bit) key in hex format openssl rand 32 | xxd -p | tr -d '\n' > /secure/path/transport.key chmod 400 /secure/path/transport.key
final_key = server_key ⊕ operator_keyGenerate split keys:
# Generate the final GELI key (this will be stored securely off-site) openssl rand 512 > /secure/offsite/final_geli.key # Generate operator key openssl rand 512 > /media/operator/operator.key # Generate server key (XOR of final and operator keys) # This requires the makeGeliKey utility from ZFS_Utils /usr/local/opt/zfs_utils/utilities/makeGeliKey \ /secure/offsite/final_geli.key \ /media/operator/operator.key \ /secure/server/server.key
If transport drive compromised:
# Generate new symmetric key openssl rand 32 | xxd -p | tr -d '\n' > /secure/path/new_transport.key # Deploy as maintenance script on next run # Old data on compromised drive remains encrypted with old key
If operator key compromised:
# Generate new operator key openssl rand 512 > /media/operator/new_operator.key # Retrieve final GELI key from secure off-site storage # Regenerate server key using makeGeliKey /usr/local/opt/zfs_utils/utilities/makeGeliKey \ /secure/offsite/final_geli.key \ /media/operator/new_operator.key \ /secure/server/server.key # Operator must use new key on next visit
Key rotation can be automated through maintenance scripts. New keys deployed during normal replication cycles without requiring emergency site visits.
Usage: sneakernet [OPTIONS] Options: -n, --dryrun Run in dry-run mode (no writes, shows what would happen) -v, --verbosity Increase verbosity level (repeat for more detail) -V, --version Display version number -h, --help Display this help message Verbosity Levels: 0 = Errors and critical messages only 1 = Standard operations 2 = Detailed operations 3 = Debugging information 4 = Snapshot lists 5 = Full detailed output
Examples:
# Test configuration without making changes sneakernet --dryrun # Run with detailed logging sneakernet -vv # Maximum verbosity for troubleshooting sneakernet -vvvvv
Maintenance scripts are Perl scripts that execute on the target server after replication completes.
Script Requirements:
cleanUpScriptsDir on transportExample maintenance script:
#!/usr/bin/env perl # cleanup_old_snapshots.pl # Remove snapshots older than 90 days use strict; use warnings; my @errors; my $pool = 'backup'; my $cutoff_days = 90; # Get list of snapshots my @snapshots = `zfs list -t snapshot -o name -s creation -H $pool`; foreach my $snap (@snapshots) { chomp $snap; # Parse timestamp from snapshot name and check age # Remove if older than cutoff # (implementation details omitted for brevity) } # Return errors array (empty = success) return @errors;
Deploy maintenance script:
# On source server: encrypt and copy to cleanup directory openssl enc -aes-256-cbc -salt -in cleanup_script.pl \ -out /configured/cleanup/dir/cleanup_script.pl.enc \ -pass file:/secure/transport.key
Source server sends email reports via configured SMTP:
Report Contents:
Configuration:
source: report: subject: "Sneakernet Replication Report" emailTo: admin@example.com emailFrom: backup@example.com
Target server writes reports to removable media:
Report Drive Configuration:
target: report: targetDrive: label: report_drive fstype: ufs mountPoint: /mnt/report
Report Contents:
This implementation balances security with operational efficiency:
Security Advantages:
Operational Advantages:
| Problem | Solution |
|---|---|
| Transport drive not detected | Verify GPT label matches config, check dmesg for device |
| Decryption fails on target | Verify transport key matches on both servers |
| Serial.txt already exists | Previous run not completed on target, investigate |
| Serial.txt missing on target | Drive already processed or not created on source |
| GELI disks won't unlock | Verify operator key present, check XOR key generation |
| Server won't shutdown | Check shutdownAfterReplication config, review logs |
# Run with maximum verbosity and dry-run sneakernet --dryrun -vvvvv 2>&1 | tee debug.log # Check ZFS_Utils log file tail -f /tmp/zfs_utils.log # Check sneakernet log file (if configured) tail -f /path/to/sneakernet.log
logFile in YAML (default: sneakernet.log)/tmp/zfs_utils.logstatusFile (tracks last replicated snapshots)
See CHANGELOG.md in the repository for complete revision history.
Repository: http://svn.dailydata.net/svn/zfs_utils/trunk
Checkout: svn export http://svn.dailydata.net/svn/zfs_utils/trunk zfs_utils
Author: R. W. Rodolico
License: BSD 2-Clause (FreeBSD License)
Company: Daily Data Inc. (https://dailydata.net)
For bug reports, feature requests, or questions, contact the repository maintainer by contact form https://dailydata.net/contact-us/