Purpose: Secure long-term backup storage isolated from network threats
Key Requirements: Full disk encryption, physical security, validated data transfer
Target Environment: FreeBSD with ZFS and GELI encryption
Status: Powered off when not actively receiving updates
An Air Gap Server is a server that operates without network connectivity to protect critical backup data from remote attacks. A modified approach allows temporary network access for system updates while maintaining security boundaries.
The primary purpose is to store long-term backups with significantly reduced attack surface. Physical isolation combined with encryption provides defense-in-depth against:
Critical: If the server must be stored in an unsecured location, full disk encryption is mandatory, not optional.
Ideal Configuration:
Fallback for Insecure Locations:
When secure facilities are unavailable:
GELI or equivalent)Store encryption keys in a different physical location than the server. Consider splitting keys across multiple secure locations.
This implementation uses FreeBSD with GELI disk encryption backing a ZFS filesystem.
At Rest Protection:
GELI (minimum requirement)Split-Key Architecture:
For enhanced security, consider using split-key encryption where the final encryption key is derived from combining two separate key components. This enhances security by allowing the actual GELI key to be stored securely off-site, as it cannot be reconstructed without both components:
In Transit Protection:
Example GELI Setup:
# Generate a random key file (4096 bits = 512 bytes) openssl rand 512 > /secure/path/geli.key chmod 400 /secure/path/geli.key # Initialize GELI encryption on disk using the key file geli init -s 4096 -K /secure/path/geli.key /dev/ada0 # Attach encrypted device geli attach -k /secure/path/geli.key /dev/ada0 # Create ZFS pool on encrypted device zpool create backup /dev/ada0.eli
Key size of 4096 bits provides strong encryption. The key file should be stored securely and backed up to a separate location. Use -P flag to add passphrase protection in addition to key file.
Transport Media Requirements:
GELI, LUKS, or BitLocker) or Encryption of individual files in transitDelta Monitoring:
Monitor transfer sizes to detect anomalies:
Data Integrity Verification:
# Generate checksum on source zfs send pool/dataset@snapshot | tee >(sha256) > /mnt/transport/delta.zfs # Verify checksum on air gap server sha256 /mnt/transport/delta.zfs
Data Validation:
Air gap servers require special consideration for maintenance since they lack network access for updates.
Validated Script Execution:
Scripts may be deployed to perform maintenance tasks:
ZFS scrubs and pool health checksScript Deployment Process:
Example Script Encryption/Decryption:
# On source server: encrypt script openssl enc -aes-256-cbc -salt -in cleanup_script.sh \ -out cleanup_script.sh.enc -pass file:/secure/transport.key # On air gap server: decrypt and execute openssl enc -aes-256-cbc -d -in cleanup_script.sh.enc \ -out cleanup_script.sh -pass file:/secure/transport.key && \ sh cleanup_script.sh || { echo "Decryption failed - aborting"; exit 1; }
Security through decryption: Scripts that cannot be decrypted with the correct symmetric key are rejected. Any decryption failure terminates the entire process to prevent execution of potentially tampered scripts.
Reporting Challenges:
Solution — Report Drive:
Report Contents:
Example Report Structure:
=== Air Gap Backup Report ===
Date: 2026-01-18 03:00:00
Operation: Incremental Backup
Source: production.example.com
Target: airgap-backup01
Datasets Processed:
- pool/data: 45.2 GB transferred
Latest: pool/data@2026-01-18_02:00:00
- pool/databases: 12.8 GB transferred
Latest: pool/databases@2026-01-18_02:00:00
Pool Health: ONLINE
Disk Status: All disks PASSED SMART checks
Maintenance Scripts Executed:
- snapshot_cleanup.sh: SUCCESS (removed 3 old snapshots)
- zfs_scrub.sh: SUCCESS (no errors found)
System Shutdown: 2026-01-18 03:45:00
Next Expected Update: 2026-01-25
Default State: Powered Off
The air gap server should remain powered off except during:
Benefits of Power-Off Strategy:
Automated Shutdown:
Final script in maintenance chain should power off the system:
#!/bin/sh # Final maintenance script - shutdown system # Verify all operations completed successfully if [ -f /var/run/backup_complete ]; then # Write final report echo "Backup completed successfully at $(date)" >> /mnt/report/status.log # Sync all filesystem buffers sync # Unmount transport media umount /mnt/transport umount /mnt/report # Power off system shutdown -p now else echo "ERROR: Backup did not complete. Manual intervention required." >> /mnt/report/error.log # Do NOT shutdown - leave powered on for troubleshooting fi
Do not configure automatic shutdown if backups fail. A powered-on system indicates problems requiring manual investigation.
A typical weekly backup cycle:
Day 1 (Monday) — Source Server:
Day 2 (Tuesday) — Physical Transport:
Day 3 (Wednesday) — Air Gap Server:
Day 4 (Thursday) — Report Processing:
Day 8 (Next Monday):
[ ] Physical Security
[ ] Secure location identified and documented
[ ] Access procedures established
[ ] Key storage locations determined
[ ] Hardware
[ ] Air gap server procured and tested
[ ] Transport drives procured (minimum 2 for rotation)
[ ] Report drive procured
[ ] All drives labeled appropriately
[ ] Encryption
[ ] GELI encryption configured and tested
[ ] Encryption keys generated and stored securely
[ ] Key recovery procedures documented
[ ] Transport drives encrypted
[ ] Software
[ ] FreeBSD installed and hardened
[ ] ZFS pools created and tested
[ ] Replication scripts developed and tested
[ ] Maintenance scripts developed and tested
[ ] Symmetric transport keys generated and deployed
[ ] Procedures
[ ] Backup schedule documented
[ ] Transport procedures documented
[ ] Report review procedures documented
[ ] Key rotation schedule established
[ ] Disaster recovery plan created
[ ] Testing
[ ] Full backup cycle tested end-to-end
[ ] Recovery procedures tested
[ ] Failure scenarios tested
[ ] Report generation verified
[ ] Automated shutdown verified
Threat Model:
This design protects against:
This design does NOT fully protect against:
Best Practices:
Common Issues:
| Problem | Symptom | Solution |
|---|---|---|
| Transport drive not mounting | Server unable to find /dev/gpt/label | Verify GPT label, check dmesg for device detection |
| Decryption fails | OpenSSL reports bad decrypt error | Verify correct symmetric key in use, check file integrity, investigate potential tampering or corruption |
| Large delta size | Delta exceeds baseline by 200%+ | Do not import — investigate source system for compromise or legitimate growth |
| Server won't shutdown | Remains powered on after backup | Check /var/run/backup_complete flag, review error logs on report drive |
| ZFS pool won't import | Import command fails | Verify encryption key, check pool status with zpool import -F |
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.
Custom automation scripts handle the complete workflow. Source code is available via Subversion:
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
Source Server Workflow:
Target Server Workflow:
GELI encrypted disks using combined key# Example: Simplified detection logic # this is actually accomplished within sneakernet automatically, so # not necessary. This just shows the logic used. HOSTNAME=$(hostname -s) if [ "$HOSTNAME" = "backup-source" ]; then # Source mode /usr/local/sbin/sneakernet --mode=source elif [ "$HOSTNAME" = "airgap-target" ]; then # Target mode /usr/local/sbin/sneakernet --mode=target else echo "ERROR: Unknown host" >&2 exit 1 fi
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:
Symmetric Transport Key:
Split GELI Key:
final_key = server_key ⊕ operator_keyKey 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, retrieve the geli key from secure storage in hex format, then run the following commands:
# Generate new key pair openssl rand 32 > operator.key xxd -p -c 999 operator.key > operator.key.hex # retrieve server.key from secure storage in binary format and run the following # perl on-liner on them. This is not tested. The keys are in hex, not binary # and the result is in hex (use xxd for two way processing) perl -e ' # Iterate over each byte index of the keys print join("", map { # Extract bytes and perform XOR sprintf("%02x", hex(substr($ARGV[0], $_, 2)) ^ hex(substr($ARGV[1], $_, 2)) ) } 0 .. (length($ARGV[0]) / 2 - 1) # Calculate the number of bytes ) . "\n" # Print the result ' 'operator.key.hex' 'server.key.hex' # Operator must use new key on next visit after updating the key on the air gap server # If server.key is ever lost, must
Key rotation can be automated through maintenance scripts. New keys deployed during normal replication cycles without requiring emergency site visits.
This implementation balances security with operational efficiency:
Security Advantages:
Operational Advantages: