====== 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):** ''GELI'' full 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 ''GELI'' key 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
* ''GELI'' kernel 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 ''GELI'' encrypted 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 ''shutdownAfterReplication'' 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.
===== 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 ''cleanUpScriptsDir'' on 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 ''logFile'' in 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 =====
* [[unix:freebsd:system_builds:airgap:concepts|Air Gap Server Concepts]] — Theoretical background and best practices
* [[https://docs.freebsd.org/en/books/handbook/disks/#disks-encrypting-geli|FreeBSD GELI Documentation]]
* [[https://docs.freebsd.org/en/books/handbook/zfs/|FreeBSD ZFS Handbook]]
* [[https://www.openssl.org/docs/|OpenSSL Documentation]]
===== Support =====
**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/