This is an old revision of the document!
Table of Contents
Sneakernet: Production Air Gap Implementation
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)
Overview
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:
- Automatic source/target mode detection
- Three-drive rotation minimizing site visits
- Multi-layer encryption (GELI + symmetric transport)
- Split-key architecture preventing single-point compromise
- Automated maintenance script execution
- Comprehensive reporting and audit trails
- Full dry-run testing capability
Client Requirements
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 |
Security Architecture
Encryption Layers:
- At Rest (Target):
GELIfull disk encryption on air gap server - In Transit: Symmetric key encryption for all data on transport drives
- Maintenance Scripts: Encrypted with same symmetric key
- Split-Key Design: Target
GELIkey derived from:- Server-resident key component (stored locally)
- Operator-carried key component (physical transport)
- Combined via XOR bitwise operation at decrypt time
- Target GELI key stored securely to facilitate key rotation and recovery
Split-key advantage: Neither component alone can decrypt the air gap server. Compromise of a single key (server or transport) does not expose data.
Installation
Prerequisites
- FreeBSD 13.0 or later
- ZFS filesystem
- Perl 5.28 or later
- OpenSSL
GELIkernel module (for target server encryption)- Subversion client (for checkout)
Getting the Source
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
Configuration
The sneakernet script uses YAML configuration files:
- Main configuration:
sneakernet.conf.yaml - Default structure:
sneakernet.datastructure - On first run, creates config from datastructure if missing
Key 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 mappings
Example 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
Operation Workflows
Source Server Workflow
The source server performs the following operations automatically:
- Auto-detect operating mode (source vs. target) via hostname
- Mount transport drive using GPT label detection
- Verify transport drive processed by target (check serial.txt)
- Securely erase previous data from transport drive
- Calculate incremental ZFS replication stream
- Encrypt and write replication data to transport drive
- Record latest snapshots sent (update status file)
- Encrypt and write maintenance scripts to transport drive
- Create serial.txt timestamp marker
- Unmount transport drive
- Email completion report to administrators
Command:
/usr/local/opt/zfs_utils/sneakernet/sneakernet
Target Server Workflow
The target server performs the following operations automatically:
- Mount transport drive
- Verify serial.txt exists (indicates unprocessed data)
- Detect operator-provided secure key (USB/separate media)
- Combine server key with operator key (XOR operation)
- Unlock
GELIencrypted disks using combined key - Import ZFS pool
- Save current snapshot list to state file (enable rollback if needed)
- Decrypt and import replication streams from transport
- Remove serial.txt (marks data as processed)
- Collect system statistics (pool health, disk status, capacity)
- Decrypt and execute maintenance scripts
- Generate detailed report and write to report drive
- Unmount all media
- Power off system (if
shutdownAfterReplicationenabled)
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.
Three-Drive Rotation Strategy
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:
- Each site visit handles both delivery and pickup
- No waiting time for drive processing
- Reduced frequency of site access (security benefit)
- Built-in offline backup (data exists on multiple drives)
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
Key Management
Symmetric Transport Key
- Unique key per deployment
- Stored on both source and target servers
- Used to encrypt data and scripts on transport drives
- 256-bit AES encryption (AES-256-CBC mode)
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
Split GELI Key
- Server component: Stored on target server (never leaves facility)
- Operator component: Carried by trusted operator (never stored at target)
- Combined at runtime via XOR:
final_key = server_key ⊕ operator_key
Generate 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
Key Rotation Procedures
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.
Command-Line Options
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
Maintenance scripts are Perl scripts that execute on the target server after replication completes.
Script Requirements:
- Must be valid Perl code
- Return empty array for success, array of error strings for failures
- Encrypted with transport symmetric key
- Stored in configured
cleanUpScriptsDiron transport
Example 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
Monitoring and Reports
Source Server Reporting
Source server sends email reports via configured SMTP:
Report Contents:
- Datasets replicated
- Snapshot names and sizes
- Total data transferred
- Disk space utilization
- Any errors or warnings
Configuration:
source: report: subject: "Sneakernet Replication Report" emailTo: admin@example.com emailFrom: backup@example.com
Target Server Reporting
Target server writes reports to removable media:
Report Drive Configuration:
target: report: targetDrive: label: report_drive fstype: ufs mountPoint: /mnt/report
Report Contents:
- Timestamp of operation
- Datasets imported
- Pool health status
- Disk SMART status
- Maintenance script results
- Any errors or warnings
Operational Benefits
This implementation balances security with operational efficiency:
Security Advantages:
- No single point of key compromise
- Lost transport drive: data remains encrypted
- Lost operator key: server data still protected
- Automated key rotation capability
- Audit trail via detailed reports
- Decryption failure = automatic abort
Operational Advantages:
- Minimal site visits (monthly vs. weekly)
- No waiting time for processing
- Fully automated operation (no manual commands)
- Email reports from source (connected)
- Physical reports from target (air-gapped)
- Automated maintenance without network access
- Dry-run mode for testing changes
Troubleshooting
Common Issues
| 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 |
Debug Mode
# 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
Log Files
- Main log: Configured via
logFilein YAML (default:sneakernet.log) - ZFS_Utils log:
/tmp/zfs_utils.log - Status file: Configured via
statusFile(tracks last replicated snapshots) - State file: Target only, records pre-update snapshot state
Version History
- v1.3.2 (2026-01-18) - Report drive notification, isMounted() integration
- v1.3.1 (2026-01-18) - CamelCase configuration keys, initialization refactoring
- v1.3.0 (2026-01-18) - Enhanced logging with caller tracking
- v1.2.x - Serial.txt mechanism, cleanup scripts, random IVs, encryption enhancements
- v1.1.x - Snapshot filtering, improved logging
- v1.0 (2025-12-15) - Initial release
See CHANGELOG.md in the repository for complete revision history.
References
- Air Gap Server Concepts — Theoretical background and best practices
Support
Repository: http://svn.dailydata.net/svn/zfs_utils/trunk
Copy: 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/
