- Update package.json version to v1.4.7 - Update README.md header to v1.4.7 - Update GEMINI.md version references to v1.4.7 - Update RECOVERY_PLAYBOOK.md version to v1.4.7 - Update SECURITY_AUDIT_REPORT.md version to v1.4.7 - Move documentation files to doc/ directory for better organization - Add new documentation files: LOCAL_TESTING_GUIDE.md, SERVE.md, TAILS_OFFLINE_PLAYBOOK.md - Add Makefile and serve.ts for improved development workflow
16 KiB
Tails Offline Air-Gapped Workflow Playbook
Overview
This playbook provides step-by-step instructions for using seedpgp-web in a secure, air-gapped environment on Tails, eliminating network exposure entirely.
Phase 1: Prerequisites & Preparation
1.1 Requirements
- Machine A (Build Machine): macOS with Bun, TypeScript, and Git installed
- Tails USB: 8GB+ USB drive with Tails installed (from tails.boum.org)
- Application USB: Separate 2GB+ USB drive for seedpgp-web
- Network: Initial internet access on Machine A only
1.2 Verify Prerequisites on Machine A (macOS with Bun)
# Verify Bun is installed
bun --version # Should be v1.0+
# Verify TypeScript tools
which tsc
# Verify git
git --version
# Clone repository
cd ~/workspace
git clone <repository-url> seedpgp-web
cd seedpgp-web
1.3 Security Checklist Before Starting
- Machine A (macOS) is trusted and malware-free (or at minimum risk)
- Bun is installed and up-to-date
- Tails USB is downloaded from official tails.boum.org
- You have physical access to verify USB connections
- You understand this is offline-only after transfer to Application USB
Phase 2: Build Application Locally (Machine A)
2.1 Clone and Verify Code
cd ~/workspace
git clone https://github.com/seedpgp/seedpgp-web.git
cd seedpgp-web
git log --oneline -5 # Document the commit hash for reference
2.2 Install Dependencies with Bun
# Use Bun for faster installation
bun install
2.3 Code Audit (CRITICAL)
Before building, audit the source for security issues:
- Review
src/lib/seedpgp.ts- main crypto logic - Review
src/lib/seedblend.ts- seed blending algorithm - Check
src/lib/bip39.ts- BIP39 implementation - Verify no external API calls in code
- Run
grep -r "fetch\|axios\|http\|api" src/to find network calls - Confirm all dependencies in
bunfig.tomlandpackage.jsonare necessary
# Perform initial audit with Bun
bun run audit # If audit script exists
grep -r "fetch\|axios\|XMLHttpRequest" src/
grep -r "localStorage\|sessionStorage" src/ # Check what data persists
2.4 Build Production Bundle Using Makefile
# Using Makefile (recommended)
make build-offline
# Or directly with Bun
bun run build
This generates:
dist/index.html- Main HTML filedist/assets/- Bundled JavaScript, CSS (using relative paths)- All static assets
2.5 Verify Build Output & Test Locally
# List all generated files
find dist -type f
# Verify no external resource links
grep -r "cloudflare\|googleapis\|cdn\|http:" dist/ || echo "✓ No external URLs found"
# Test locally with Bun's simple HTTP server
bun ./dist/index.html
# Or serve on port 8000
bun --serve --port 8000 ./dist # deprecated: Bun does not provide a built-in static server
# Use the Makefile: `make serve-local` (runs a Python http.server) or run directly:
# cd dist && python3 -m http.server 8000
# Then open http://localhost:8000 in Safari
Why not file://?: Safari and Firefox restrict loading local assets via file:// protocol for security. Using a local HTTP server bypasses this while keeping everything offline.
Phase 3: Prepare Application USB (Machine A - macOS with Bun)
3.1 Format USB Drive
# List USB drives
diskutil list
# Replace diskX with your Application USB (e.g., disk2)
diskutil secureErase freespace 0 /dev/diskX
# Create new partition
diskutil partitionDisk /dev/diskX 1 MBR FAT32 SEEDPGP 0b
3.2 Copy Built Files to USB
# Mount should happen automatically, verify:
ls /Volumes/SEEDPGP
# Copy entire dist folder built with make build-offline
cp -R dist/* /Volumes/SEEDPGP/
# Verify copy completed
ls /Volumes/SEEDPGP/
ls /Volumes/SEEDPGP/assets/
# (Optional) Generate integrity hash for verification on Tails
sha256sum dist/* > /Volumes/SEEDPGP/INTEGRITY.sha256
3.3 Verify USB Contents
# Ensure index.html exists and is readable
cat /Volumes/SEEDPGP/index.html | head -20
# Check file count matches
echo "Source files:" && find dist -type f | wc -l
echo "USB files:" && find /Volumes/SEEDPGP -type f | wc -l
# Check assets are properly included
find /Volumes/SEEDPGP -type d -name "assets" && echo "✅ Assets folder present"
# Verify no external URLs in assets
grep -r "http:" /Volumes/SEEDPGP/ && echo "⚠️ Warning: HTTP URLs found" || echo "✅ No external URLs"
3.4 Eject USB Safely
diskutil eject /Volumes/SEEDPGP
Phase 4: Boot Tails & Prepare Environment
4.1 Boot Tails from Tails USB
- Power off machine
- Insert Tails USB
- Power on and boot from USB (Cmd+Option during boot on Mac)
- Select "Start Tails"
- DO NOT connect to network (decline "Connect to Tor" if prompted)
4.2 Insert Application USB
- Once Tails is running, insert Application USB
- Tails should auto-mount it to
/media/amnesia/<random-name>/
4.3 Verify Files Accessible & Start HTTP Server
# Open terminal in Tails
ls /media/amnesia/
# Should see your Application USB mount
# Navigate to application
cd /media/amnesia/SEEDPGP/
# Verify files are present
ls -la
cat index.html | head -5
# Start local HTTP server (runs completely offline)
python3 -m http.server 8080 &
# Output: Serving HTTP on 0.0.0.0 port 8080
# Verify server is running
curl http://localhost:8080/index.html | head -5
Note: Python3 is pre-installed on Tails. The http.server runs completely offline—no internet access required, just localhost.
Phase 5: Run Application on Tails
5.1 Open Application in Browser via Local HTTP Server
Why HTTP instead of file://?
- HTTP is more reliable than
file://protocol - Eliminates browser security restrictions
- Identical to local testing on macOS
- Still completely offline (no network exposure)
Steps:
-
In Terminal (where you started the server from Phase 4.3):
- Verify server is still running:
ps aux | grep http.server - Should show:
python3 -m http.server 8080 - If stopped, restart:
cd /media/amnesia/SEEDPGP && python3 -m http.server 8080 &
- Verify server is still running:
-
Open Firefox:
- Click Firefox icon on desktop
- In address bar, type:
http://localhost:8080 - Press Enter
-
Verify application loaded:
- Page should load completely
- All UI elements visible
- No errors in browser console (F12 → Console tab)
5.2 Verify Offline Functionality
- Page loads completely
- All UI elements are visible
- No error messages in browser console (F12)
- Images/assets display correctly
- No network requests are visible in Network tab (F12)
5.3 Test Application Features
Basic Functionality:
- [ ] Can generate new seed phrase
- [ ] Can input existing seed phrase
- [ ] Can encrypt seed phrase
- [ ] Can generate PGP key
- [ ] QR codes generate correctly
Entropy Sources (all should work offline):
- Dice entropy input works
- User mouse/keyboard entropy captures
- Random.org is NOT accessible (verify UI indicates offline mode)
- Audio entropy can be recorded
5.4 Generate Your Seed Phrase
- Navigate to main application
- Choose entropy source (Dice, Audio, or Interaction)
- Follow prompts to generate entropy
- Review generated 12/24-word seed phrase
- Write down on paper (do NOT screenshot, use only pen & paper)
- Verify BIP39 validation passes
Phase 6: Secure Storage & Export
6.1 Export Encrypted Backup (Optional)
If you want to save encrypted backup to USB:
- Use application's export feature
- Encrypt with strong passphrase
- Save to Application USB
- Do NOT save to host machine
6.2 Generate PGP Key (Optional)
- Use seedpgp-web to generate PGP key
- Export private key (encrypted)
- Save encrypted to USB if desired
- Passphrase should be memorable but not written
6.3 Verify No Leaks
In Firefox Developer Tools (F12):
- Network tab: Should show only
localhost:8080requests (all local) - Application/Storage: Check nothing persistent was written
- Console: No fetch/XHR errors to external sites
To verify server is local-only:
# In terminal, check network connections
sudo netstat -tulpn | grep 8080
# Should show: tcp 0 0 127.0.0.1:8080 (LISTEN only on localhost)
# NOT on 0.0.0.0 or external interface
Phase 7: Shutdown & Cleanup
7.1 Stop HTTP Server & Secure Shutdown
# Stop the http.server gracefully
killall python3
# Or find the PID and kill it
ps aux | grep http.server
kill -9 <PID> # Replace <PID> with actual process ID
# Verify it stopped
ps aux | grep http.server # Should show nothing
# Then power off Tails completely
sudo poweroff
# You can also:
# - Select "Power Off" from Tails menu
# - Or simply close/restart the laptop
Important: Killing the server ensures no background processes remain before shutdown.
7.2 Physical USB Handling
- Eject Application USB physically
- Eject Tails USB physically
- Store both in secure location
- Tails memory is volatile - all session data gone after power-off
7.3 Host Machine Cleanup (Machine A)
# Remove build artifacts if desired (optional)
rm -rf dist/
# Clear sensitive files from shell history
history -c
# Optionally wipe Machine A's work directory
rm -rf ~/workspace/seedpgp-web/
Phase 8: Verification & Best Practices
8.1 Before Production Use - Full Test Run
Test on macOS with Bun first:
cd seedpgp-web
bun install
make build-offline # Build with relative paths
make serve-local # Serve on http://localhost:8000
# Open Safari: http://localhost:8000
# Verify: all assets load, no console errors, no network requests
- Complete Phases 1-7 with test run on Tails
- Verify seed phrase generation works reliably
- Test entropy sources work offline
- Confirm PGP key generation (if using)
- Verify export/backup functionality
8.2 Security Best Practices
- Air-gap is primary defense: No network = no exfiltration
- Tails is ephemeral: Always boot fresh, always clean shutdown
- Paper backups: Write seed phrase with pen/paper only
- Multiple USBs: Keep Tails and Application USB separate
- Verify hash: Optional - generate hash of
dist/folder to verify integrity on future builds
8.3 Future Seed Generation
Repeat these steps for each new seed phrase:
-
Boot Tails from Tails USB (network disconnected)
-
Insert Application USB when Tails is running
-
Start HTTP server:
cd /media/amnesia/SEEDPGP python3 -m http.server 8080 & -
Open Firefox →
http://localhost:8080 -
Generate seed phrase (choose entropy source)
-
Write on paper (pen & paper only, no screenshots)
-
Stop server and shutdown:
killall python3 sudo poweroff
Troubleshooting
Issue: Application USB Not Mounting on Tails
Solution:
# Check if recognized
sudo lsblk
# Manual mount
sudo mkdir -p /media/usb
sudo mount /dev/sdX1 /media/usb
ls /media/usb
Issue: Black Screen / Firefox Won't Start
Solution:
- Let Tails fully boot (may take 2-3 minutes)
- Try manually starting Firefox from Applications menu
- Check memory requirements (Tails recommends 2GB+ RAM)
Issue: Assets Not Loading (Broken Images/Styling)
Solution:
# Verify file structure on USB
ls -la /media/amnesia/SEEDPGP/assets/
# Check permissions
chmod -R 755 /media/amnesia/SEEDPGP/
Issue: Browser Console Shows Errors
Solution:
- Check if
index.htmlreferences external URLs - Verify
vite.config.tsdoesn't have external dependencies - Review network tab - should show only
localhost:8080requests
Issue: Can't Access http://localhost:8080
Solution:
# Verify http.server is running
ps aux | grep http.server
# If not running, restart it
cd /media/amnesia/SEEDPGP
python3 -m http.server 8080 &
# If port 8080 is in use, try another port
python3 -m http.server 8081 &
# Then access http://localhost:8081
Issue: "Connection refused" in Firefox
Solution:
# Check if port is listening
sudo netstat -tulpn | grep 8080
# If not, the server stopped. Restart it:
cd /media/amnesia/SEEDPGP
python3 -m http.server 8080 &
# Wait a few seconds and refresh Firefox (Cmd+R or Ctrl+R)
Security Checklist Summary
Before each use:
- Using Tails booted from USB (never host OS)
- Application USB inserted (separate from Tails USB)
- Network disconnected or Tor disabled
- HTTP server started:
python3 -m http.server 8080from USB - Accessing http://localhost:8080 (not file://)
- Firefox console shows no external requests
- All entropy sources working offline
- Seed phrase written on paper only
- HTTP server stopped before shutdown
- USB ejected after use
- Tails powered off completely
Appendix A: Makefile Commands Quick Reference
All build commands are available via Makefile on Machine A:
make help # Show all available commands
make install # Install Bun dependencies
make build-offline # Build with relative paths (for Tails/offline)
make serve-local # Test locally on http://localhost:8000
make audit # Security audit for network calls
make verify-offline # Verify offline compatibility
make full-build-offline # Complete pipeline: clean → build → verify → audit
make clean # Remove dist/ folder
Example workflow:
cd seedpgp-web
make install
make audit # Security check
make full-build-offline # Build and verify
# Copy to USB when ready
Appendix B: Local Testing on macOS Before Tails
Why test locally first?
- Catch build issues early
- Verify all assets load correctly
- Confirm no network requests
- Validation before USB transfer
Steps:
cd seedpgp-web
make build-offline # Build with relative paths
make serve-local # Start local server
# Open Safari: http://localhost:8000
# Test functionality, then Ctrl+C to stop
When served locally on http://localhost:8000, assets load correctly. On Tails via http://localhost:8080, the same relative paths work seamlessly.
Appendix C: Why HTTP Server Instead of file:// Protocol?
The Problem with file:// Protocol
Opening file:///path/to/index.html directly has limitations:
- Browser security restrictions - Some features may be blocked or behave unexpectedly
- Asset loading issues - Sporadic failures with relative/absolute paths
- localStorage limitations - Storage APIs may not work reliably
- CORS restrictions - Even local files face CORS-like restrictions on some browsers
- Debugging difficulty - Hard to distinguish app issues from browser security issues
Why http.server Solves This
Python's http.server module:
- Mimics production environment - Behaves like a real web server
- Completely offline - Server runs only on localhost (127.0.0.1:8080)
- No internet required - No connection to external servers
- Browser compatible - Works reliably across Firefox, Safari, Chrome
- Identical to macOS testing - Same mechanism for both platforms
- Simple & portable - Python3 comes pre-installed on Tails
Verify the server is local-only:
sudo netstat -tulpn | grep 8080
# Output should show: 127.0.0.1:8080 LISTEN (localhost only)
# NOT 0.0.0.0:8080 (would indicate public access)
This ensures your seedpgp-web app runs in a standard HTTP environment without any network exposure. ✅
Additional Resources
- Tails Documentation: https://tails.boum.org/doc/
- seedpgp-web Security Audit: See SECURITY_AUDIT_REPORT.md
- BIP39 Standard: https://github.com/trezor/python-mnemonic
- Air-gap Best Practices: https://en.wikipedia.org/wiki/Air_gap_(networking)
- Bun Documentation: https://bun.sh/docs
- Python HTTP Server: https://docs.python.org/3/library/http.server.html
Version History
- v2.1 - February 13, 2026 - Updated to use Python http.server instead of file:// for reliability
- v2.0 - February 13, 2026 - Updated for Bun, Makefile, and offline compatibility
- v1.0 - February 13, 2026 - Initial playbook creation