A hardened Docker environment that isolates your crypto wallets behind a whitelist-only egress proxy and a LUKS-encrypted disk.
Run MetaMask, Rabby & Phantom in a locked-down browser — even if it's compromised, stolen data can't phone home, and on-disk files are AES-encrypted.
The Problem • How It Protects You • Architecture • Quick Start • Encryption • Hardware Wallets • Threat Model
Your crypto wallets and your daily browsing/coding share the same computer. That means:
- A bad
npm installcan read your MetaMask files and steal your keys - A malicious website can drop malware that drains every wallet on your machine
- Even if malware gets your wallet data, it can silently send it to a hacker's server — and you'd never know until the funds are gone
This is not hypothetical. North Korean hackers (Lazarus Group) have stolen billions this way — posing as recruiters, sending devs a repo to clone, and npm install does the rest.
crypto-safe-container puts your wallets inside a locked box (Docker container) with three simple rules:
The container is isolated. Malware inside it cannot read your host files, other browsers, SSH keys, or anything outside the box. Your wallets live in a separate world.
Even if something bad gets inside the box, it cannot send your data to a hacker's server. All internet traffic goes through a proxy that only allows whitelisted domains — Ethereum RPCs, Uniswap, Aave, block explorers, etc. Everything else is blocked.
Normal setup: Malware steals keys → sends to evil.com → funds gone
With this project: Malware steals keys → tries evil.com → BLOCKED by proxy
→ tries any site → BLOCKED
→ data goes nowhere
The vault profile (wallet data, browser history, cookies) lives inside a LUKS-encrypted disk image at ~/.crypto-vault/vault.luks. When the vault isn't running, the file on disk is meaningless ciphertext — even the host can't read it without the passphrase. AES-256-XTS, Argon2id KDF; same crypto your laptop's full-disk encryption uses. See Encrypted-at-rest mode for details.
Vault not running: `vault.luks` on disk → encrypted bytes → useless
Vault running: `./vault.sh up` → passphrase → decrypted in memory only
Only what you need for DeFi — nothing more:
- RPC providers (Infura, Alchemy, Ankr)
- DeFi protocols (Uniswap, Aave, Curve, Lido, Jupiter, Raydium)
- Block explorers (Etherscan, Solscan)
- Wallet updates (MetaMask, Phantom, Rabby)
- Hardware wallet bridges (Ledger, Trezor)
You control the list. Add or remove domains anytime in squid/whitelist.txt.
| Attack | What happens normally | With crypto-safe-container |
|---|---|---|
Malicious npm install |
Reads your Chrome profile, steals wallet keys, sends to attacker | Container has no access to your host Chrome profile |
| Fake DApp drains wallet | You sign a bad transaction, malware exfiltrates your seed | Transaction goes through (your call), but seed can't be exfiltrated to non-whitelisted servers |
| Clipboard hijacker | Replaces your copied wallet address with attacker's | Malware is inside the container — can't touch your host clipboard |
VS Code tasks.json exploit |
Opens a folder, auto-runs malware, steals everything | Your wallets aren't on the same machine (they're in the container) |
| Laptop stolen / disk imaged | Wallet files are on disk as plaintext, brute-forcing only needs to defeat the wallet password | LUKS image is encrypted ciphertext; attacker has to crack LUKS and the wallet password — two independent Argon2id/scrypt KDFs |
| Backup of your home dir leaks (OneDrive, Time Machine) | Your wallet's encrypted seed blob is in the backup, ready for offline brute-force | Only the LUKS ciphertext is in the backup — useless without the passphrase |
A hardware wallet (Ledger/Trezor) is still the strongest protection. This project is the next best thing for software wallets, and works great alongside hardware wallets too (USB passthrough supported).
┌──────────────────────────────────────────────────────────┐
│ Your Host Machine │
│ │
│ X11 ─────── Chromium window (native, pixel-perfect) │
│ │ │
│ ┌──────────────────┼───────── vault-internal ───────┐ │
│ │ │ (no internet) │ │
│ │ ┌──────────────▼────────────────────────────┐ │ │
│ │ │ crypto-vault (172.30.0.10) │ │ │
│ │ │ │ │ │
│ │ │ Chromium (X11 forwarding to host) │ │ │
│ │ │ MetaMask / Phantom / Rabby │ │ │
│ │ │ Foundry (forge / cast / anvil) │ │ │
│ │ │ Node.js 20 + Hardhat + ethers.js │ │ │
│ │ │ │ │ │
│ │ │ --cap-drop ALL │ │ │
│ │ │ no-new-privileges │ │ │
│ │ │ Profile dir → bind to LUKS mount ┐ │ │ │
│ │ └────────────────────────────────────┼──────┘ │ │
│ │ │ proxy:3128 │ │ │
│ │ ┌──────────────▼────────────────┐ ▼ │ │
│ │ │ egress-proxy (172.30.0.2) │ ┌──────────┐ │ │
│ │ │ Squid → whitelist.txt ONLY │ │ LUKS2 │ │ │
│ │ └──────────────────┬────────────┘ │ image │ │ │
│ │ │ │ (AES-XTS)│ │ │
│ └──────────────────────┼────────────────┴──────────┘ │
│ ▼ │
│ Internet │
└──────────────────────────────────────────────────────────┘
Key security properties:
- No direct internet — The vault sits on an
internal: trueDocker network - Whitelist-only egress — All traffic passes through a Squid proxy; only ~50 explicitly-listed DeFi/Web3 domains are reachable
- Encrypted at rest by default — Vault profile lives inside a LUKS2 disk image (AES-256-XTS, Argon2id KDF); ciphertext on disk when vault is locked
- Hardened container —
cap_drop: [ALL],no-new-privileges, non-root user (UID 1000), PID/memory limits - Extension allowlist — Chrome policy blocks every extension except MetaMask, Phantom, Rabby (by ID)
- Native X11 rendering — Chromium runs as a native window — no VNC blur, no compression
- npm locked down —
ignore-scripts=trueglobally preventspostinstallRCE inside the vault - Minimal base —
debian:bookworm-slim+ Debian'schromium(no Google binary blob), ~1.9 GB image - Hardware wallet ready — USB passthrough for Ledger/Trezor (keys never touch software)
Prerequisites: Docker Engine 20.10+ and Docker Compose v2
X11 works out of the box (WSLg on WSL2, native X server on Linux).
git clone https://github.com/lywedo/crypto-safe-container.git
cd crypto-safe-container
./vault.sh upOn first run, ./vault.sh up walks you through three prompts:
- Install
cryptsetup(one-time host dependency for LUKS) — auto-installs via apt/pacman/dnf withsudo. - Create a LUKS passphrase — encrypts the vault profile on disk. Use ≥20 random characters from a password manager.
- Chromium opens with a welcome page linking to MetaMask, Phantom, and Rabby on the Chrome Web Store.
Subsequent runs just ask for the LUKS passphrase and launch. When you close Chromium, the disk auto-locks.
Optional: Add to your app launcher / Start Menu
make install-linux
# Search "Crypto Vault" in your app launcherRequires XQuartz for X11 support. LUKS isn't available on macOS — use FileVault for full-disk encryption instead, and the vault falls back to a plain Docker volume (it prints a warning at startup).
brew install --cask xquartz
# Log out and back in after installing XQuartz
git clone https://github.com/lywedo/crypto-safe-container.git
cd crypto-safe-container
VAULT_INSECURE=1 ./vault.sh up # explicit opt-out of LUKSOptional: Add to Launchpad
make install-macVAULT_INSECURE=1 ./vault.sh upThis uses an unencrypted Docker volume (the pre-LUKS default). Useful for kicking the tyres, but don't store real wallets this way.
On first launch, Chrome opens a welcome page with install links for:
| Extension | Extension ID |
|---|---|
| MetaMask — Ethereum wallet & DApp browser | nkbihfbeogaeaoehlefnkodbefgpgknn |
| Phantom — Solana, Ethereum & multi-chain wallet | bfnaelmomeimhlpmgjnjophhpkkoljpa |
| Rabby Wallet — Multi-chain wallet with security alerts | acmacodkjbdgmoleebolmdjonilkdbch |
Click "Add to Chrome" for each one. This only needs to be done once — extensions are stored in a persistent Docker volume (crypto-vault-chrome) and survive container restarts.
The Chrome managed policy in chrome-policies.json uses:
"ExtensionInstallBlocklist": ["*"],
"ExtensionInstallAllowlist": [
"nkbihfbeogaeaoehlefnkodbefgpgknn",
"bfnaelmomeimhlpmgjnjophhpkkoljpa",
"acmacodkjbdgmoleebolmdjonilkdbch"
]This blocks every Chrome Web Store extension by default, then re-permits only MetaMask, Phantom, and Rabby by ID. Attempting to install anything else — even from the Chrome Web Store — is blocked by Chrome itself, not the proxy.
This is a defence-in-depth measure: if a malicious site ever bypassed the proxy, it still couldn't sneak a rogue extension into your wallet browser.
To allow another extension, find its ID (visible in the Chrome Web Store URL: chromewebstore.google.com/detail/<name>/<ID>) and add it to ExtensionInstallAllowlist in chrome-policies.json, then rebuild:
docker compose --profile vault build crypto-vault
./vault.sh upConsider the security tradeoff carefully — every extension you add is another supply-chain risk.
Web3 CLI tools are available inside the vault and via the helper script:
# Open a shell inside the vault
./vault.sh shell
# Foundry commands (from host)
./vault.sh forge init my-project
./vault.sh forge build
./vault.sh forge test
./vault.sh cast balance vitalik.eth --rpc-url https://eth.llamarpc.com
# Hardhat (from host)
./vault.sh hardhat compile
# Or work interactively
./vault.sh shell
cd projects && npx hardhat init| Command | Description |
|---|---|
./vault.sh setup |
Check/install host deps (cryptsetup, e2fsprogs) |
./vault.sh up |
Unlock disk, launch Chromium (X11). Runs setup first. |
./vault.sh down |
Stop everything, lock the encrypted disk |
./vault.sh shell |
Bash shell in an ephemeral vault container |
./vault.sh forge <args> |
Run Foundry forge |
./vault.sh cast <args> |
Run Foundry cast |
./vault.sh hardhat <args> |
Run Hardhat |
./vault.sh disk <subcmd> |
Manage encrypted disk: init / open / close / status / resize |
./vault.sh logs [service] |
Follow container logs |
./vault.sh whitelist |
Show allowed domains |
./vault.sh reload-proxy |
Apply whitelist changes |
./vault.sh smoketest |
Verify proxy invariants |
./vault.sh backup |
Copy the encrypted disk image (or tarball if VAULT_INSECURE=1) |
./vault.sh status |
Show services + encrypted disk state |
./vault.sh nuke |
Delete ALL vault data (incl. the LUKS image) |
Environment overrides: VAULT_INSECURE=1 (skip LUKS), VAULT_SIZE=20G (image size at first init), CRYPTO_DIR=/some/path (where the LUKS image lives — default ~/.crypto-vault).
The proxy blocks everything not in squid/whitelist.txt. To add a new protocol:
# Add a domain
echo ".newprotocol.xyz" >> squid/whitelist.txt
# Apply without restarting the vault
./vault.sh reload-proxyDefault whitelist includes: Ethereum/Solana RPCs, major DeFi (Uniswap, Aave, Jupiter, Raydium), block explorers, Chrome Web Store, wallet update servers, IPFS gateways, and hardware wallet bridges. See squid/whitelist.txt for the full list.
After any change to squid/ (or just to sanity-check your install), run:
./scripts/smoketest.shThis asserts the three properties the proxy must guarantee:
- Whitelisted domain reachable —
api.etherscan.ioreturns200through the proxy - Non-whitelisted blocked —
evil.comis denied by Squid with403 Forbidden - Direct internet unreachable — bypassing the proxy from
vault-internaltimes out (network is truly isolated)
The same script runs in CI (see .github/workflows/ci.yml), so any whitelist change gets validated on every push.
The vault's Chromium profile lives inside a LUKS2-encrypted disk image at ~/.crypto-vault/vault.luks. The host sees one opaque file; opening it requires a passphrase you choose at first run.
- AES-256-XTS block-level encryption (same as Linux full-disk encryption)
- Argon2id KDF — modern, memory-hard, brute-force resistant
- One single file on disk: easy to back up, move, or delete
- Hard size cap (default 5 GB, configurable)
- Authenticated by the kernel's dm-crypt — tampering is detected on read
./vault.sh up
# > First-run setup: creating encrypted disk image...
# > [sudo password — for cryptsetup]
# > Enter LUKS passphrase: ********
# > Verify passphrase: ********
# > (creates ~/.crypto-vault/vault.luks, ~5 GB, formats LUKS + ext4)
# > Launching Chromium...Use a ≥20 character random passphrase from a password manager. If you forget it AND lose your seed phrase, the wallet is unrecoverable. (Your seed phrase remains the real backup; the passphrase protects only the convenience copy of state.)
Want a different size? VAULT_SIZE=20G ./vault.sh up on first run.
./vault.sh up
# > Enter LUKS passphrase: ********
# > Launching Chromium...
# (work, then close the browser)
# > Lock the encrypted disk: ./vault.sh disk close (or it's already done by `down`)The encrypted disk is opened before Chromium starts and closed when it exits. State is preserved across sessions inside the LUKS image.
On first ./vault.sh up, the script runs a preflight check (scripts/preflight.sh) and prompts you to install any missing host packages — cryptsetup and e2fsprogs. It auto-detects your package manager (apt / pacman / dnf / zypper).
You can also run the check explicitly:
./vault.sh setupWhy these tools aren't shipped inside the container: LUKS is a Linux kernel feature, and cryptsetup has to run on the host with access to /dev/mapper. Doing it from inside the container would require --privileged, a much bigger security trade-off than one auto-installed package.
WSL2's kernel already has the loop and dm-crypt modules, so cryptsetup is the only missing piece for most users.
macOS: LUKS is a Linux kernel feature. On macOS, use FileVault for full-disk encryption (covers the Docker volume), or manually set up an APFS-encrypted disk image. The vault will print a warning and fall back to the unencrypted Docker volume.
./vault.sh disk init [size] # Re-init (rare — usually done by first ./vault.sh up)
./vault.sh disk open # Manually unlock + mount
./vault.sh disk close # Manually unmount + lock
./vault.sh disk status # Show state (initialized? locked? mounted?)
./vault.sh disk resize 10G # Grow the image (close it first)./vault.sh backup
# (Closes the disk, then copies vault.luks to backups/<timestamp>/.)
# The backup file is already encrypted — safe to store offline.The LUKS image is the backup format. Copy vault.luks to a USB stick — anyone who finds it sees ciphertext.
VAULT_INSECURE=1 ./vault.sh upThis bypasses LUKS entirely and uses a plain Docker volume. Use only for evaluation and CI — your wallet data is unencrypted on disk in this mode. The first-run flow doesn't ask for a passphrase in this mode.
| Threat | Encrypted at rest? |
|---|---|
| Laptop stolen while powered off | Protected |
| Backup of your home dir leaks to the cloud | Protected — ciphertext is useless without the passphrase |
Malware reads ~/.crypto-vault/vault.luks while the disk is locked |
Protected |
| Malware reads the vault while it's unlocked and mounted | Not protected — plaintext is on the mount |
| Keylogger captures your passphrase as you type it | Not protected |
| Weak wallet password gets brute-forced (after the LUKS layer is broken) | Use a strong wallet password too |
sudo is compromised on the host |
Not protected — cryptsetup runs as root |
Defense-in-depth, not silver bullet. Combine with full-disk encryption (BitLocker/FileVault), a strong wallet password, and a hardware wallet for the realistic threat model.
Ledger and Trezor work via USB passthrough. Your private keys stay on the device's secure element — they never touch the container.
Step 1: Install udev rules on your host (Linux / WSL2)
# Ledger
wget -q -O - https://raw.githubusercontent.com/LedgerHQ/udev-rules/master/add_udev_rules.sh | sudo bash
# Trezor
sudo curl https://data.trezor.io/udev/51-trezor.rules -o /etc/udev/rules.d/51-trezor.rules
sudo udevadm control --reload-rulesStep 2: Uncomment USB passthrough in vault-x11.sh (add these flags to the docker run command)
--device /dev/bus/usb:/dev/bus/usb \
--device /dev/hidraw0:/dev/hidraw0 \
--group-add plugdevWhere your wallet data actually lives:
| Mode | What persists | Where |
|---|---|---|
| Default (LUKS) | Chromium profile, extensions, wallet data | Inside ~/.crypto-vault/vault.luks (one encrypted file) |
| Default (LUKS) | Downloads | Docker volume crypto-vault-downloads |
| Default (LUKS) | Foundry/Hardhat projects | Docker volume crypto-vault-projects |
VAULT_INSECURE=1 |
Chromium profile | Docker volume crypto-vault-chrome (plaintext) |
Data survives ./vault.sh down and reboots. Only ./vault.sh nuke deletes it — and nuke removes the LUKS image, the Docker volumes, and the marker file. Once nuked, your seed phrase is your only path back.
Important: the encrypted disk only protects the Chromium profile (where wallet extensions store their data). Downloads and Foundry projects use plain Docker volumes. Don't put seed phrases or private keys in
~/projects/— that directory is not encrypted.
./vault.sh backupIn default LUKS mode, this:
- Locks the encrypted disk (safely closing all writes)
- Copies
~/.crypto-vault/vault.lukstobackups/YYYYMMDD_HHMMSS/vault.luks
The backup is already encrypted — safe to put on a USB stick or in cloud storage. Anyone who finds it sees random bytes; only your LUKS passphrase can open it.
In VAULT_INSECURE=1 mode, this tars the Docker volume:
-> backups/YYYYMMDD_HHMMSS/chrome-profile.tar.gz
Encrypt before storing: gpg -c backups/*/chrome-profile.tar.gz
Your seed phrase is the real backup. Both backup formats preserve wallet settings (custom tokens, RPC endpoints, transaction history) but the seed itself is also in there, encrypted by the wallet's password. If you lose both the backup and your seed phrase, your funds are gone. If you have the seed phrase, you can always restore from scratch on a fresh vault.
The protection works in layers. Each layer defends against a different class of attack:
| Threat | Defense | Limit |
|---|---|---|
Malicious npm install |
Container has no access to host | Container shares the kernel — kernel exploits can escape (rare but real) |
| Data exfiltration over network | Squid proxy whitelist; internal: true Docker network |
DNS poisoning of an allowlisted domain is theoretically possible |
| Browser zero-day RCE in the vault | cap_drop: [ALL], no-new-privileges, non-root, internal-only network |
Can't fully prevent exploitation, but exfiltration is still blocked |
| Disk theft (powered-off laptop) | LUKS encryption (AES-256-XTS, Argon2id) | Weak passphrase → brute-forceable; combine with BitLocker/FileVault |
| Cloud backup leaking your home dir | LUKS image is ciphertext | Same: passphrase strength is everything |
| Host malware reading disk while vault is locked | LUKS encryption | None — ciphertext only |
| Host malware reading vault while unlocked | Wallet's own scrypt encryption of the seed | Strong wallet password is the only barrier; malware as root can dump LUKS keys from kernel memory |
| Malicious extension install | Chrome policy: blocklist * + allowlist [MetaMask, Phantom, Rabby] |
None — Chrome refuses |
| Clipboard hijacker on host | n/a (X11 is shared) | Not protected — use Rabby's address verification |
| Signing a malicious transaction | n/a | Not protected — read what you sign; use a hardware wallet for serious holdings |
| Your seed phrase being lost / leaked | n/a | Not protected — store on metal, offline, in two locations |
The container is rung 2 on the isolation ladder: better than running a wallet on your daily browser, not as strong as a VM or Qubes OS. For:
- Wallets > $1,000 → use a hardware wallet (Ledger/Trezor). The vault becomes the signing UI, not the key store.
- Wallets > $50,000 → also consider a dedicated VM (KVM/VirtualBox), Qubes OS, or multisig (Safe). Container isolation is real but not equivalent to hardware virtualization.
A hardware wallet remains the single strongest protection because private keys never exist in software, regardless of how compromised the host is. Everything in this project composes well with hardware wallets — USB passthrough is supported.
crypto-safe-container/
├── docker-compose.yml # Egress proxy service (always loaded)
├── docker-compose.disk.yml # LUKS-encrypted disk override (auto-loaded in default mode)
├── Dockerfile # Chromium + Foundry + Node.js image
├── vault.sh # Helper CLI (preferred entry point)
├── vault-x11.sh # Launch Chromium via X11 (called by vault.sh up)
├── chrome-policies.json # Chrome managed policy (extension allow/blocklist, etc.)
├── welcome.html # First-run wallet install page (loaded in Chromium)
├── Makefile # install-linux / install-mac targets
├── crypto-vault.desktop # Linux .desktop entry (for app launchers)
├── .dockerignore
├── .gitignore
├── squid/
│ ├── squid.conf # Proxy configuration
│ └── whitelist.txt # Allowed domains (edit this!)
├── scripts/
│ ├── preflight.sh # Host dep check + auto-install (cryptsetup, e2fsprogs)
│ ├── disk.sh # LUKS init/open/close/status/resize
│ └── smoketest.sh # Verify proxy invariants (used by CI)
└── .github/
├── workflows/ci.yml
└── ISSUE_TEMPLATE/
Contributions welcome! See CONTRIBUTING.md for guidelines.
The most impactful contributions right now:
- Whitelist additions for popular DeFi protocols
- Security hardening improvements
- Testing on different Docker/OS versions
- Documentation — especially video demos
MIT — use it, fork it, protect your crypto.
If this project helped you, consider giving it a star.
Every star helps more developers discover it before they get scammed.
