Production Docker Security: Why Your AI Wallet Should Only Listen on Localhost
Production Docker security for AI wallets starts with one simple principle: if your wallet daemon is listening on a public interface, you've already made a mistake. Whether you're running a trading bot, a payment agent, or an autonomous DeFi manager, exposing your private key infrastructure to the open internet is the crypto equivalent of leaving your front door unlocked because you live in a "safe neighborhood." This post walks through why WAIaaS defaults to localhost-only binding, what that means for your self-hosted setup, and how to deploy securely in under ten minutes.
The Problem With "Just Open the Port"
There's a tempting shortcut when you're trying to get something working quickly: change 127.0.0.1:3100:3100 to 0.0.0.0:3100:3100 in your Docker Compose file and suddenly everything just connects. Your AI agent can reach the daemon. Your remote scripts work. Problem solved.
Except it isn't. You've just told your wallet service — the thing that holds signing keys and can authorize financial transactions — to accept connections from any network interface on your machine. On a VPS, that typically means the public internet. Anyone who can reach your server's IP on port 3100 can attempt to interact with your wallet infrastructure.
The stakes here aren't theoretical. An AI wallet daemon isn't a blog or a todo app. It handles signing transactions, executing DeFi actions, and managing session tokens that can move real funds. The attack surface of a publicly exposed wallet API is categorically different from other self-hosted services.
This is why WAIaaS ships with 127.0.0.1:3100:3100 as the default — not as a limitation, but as a deliberate security stance.
Why Self-Hosting an AI Wallet Is Worth the Effort
Before diving into the Docker specifics, it's worth understanding the philosophy here. When you use a hosted wallet service for your AI agent, you're trusting someone else with:
- Your private keys (or derivation material)
- Your transaction history
- Your spending policies and limits
- The availability of your agent's financial operations
With a self-hosted setup like WAIaaS, none of that leaves your infrastructure. Your keys are generated on your hardware, stored in your data directory, and never transmitted to a third party. Your AI agent's wallet is as sovereign as your server.
This is the crypto equivalent of running your own email server — except WAIaaS actually makes it practical. One Docker image, a named volume for persistence, and a handful of environment variables gets you a fully operational wallet daemon. The tradeoff is that you are responsible for securing it, which brings us back to the localhost binding.
Understanding the Default: 127.0.0.1:3100:3100
The WAIaaS Docker Compose configuration ships with this port binding:
services:
daemon:
image: ghcr.io/waiaas/waiaas:latest
container_name: waiaas-daemon
ports:
- "127.0.0.1:3100:3100"
volumes:
- waiaas-data:/data
environment:
- WAIAAS_DATA_DIR=/data
- WAIAAS_DAEMON_HOSTNAME=0.0.0.0
env_file:
- path: .env
required: false
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3100/health"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
volumes:
waiaas-data:
driver: local
Notice 127.0.0.1:3100:3100 in the ports section. This binds the daemon to the loopback interface only. Traffic on your local machine can reach it — your AI agent running on the same host, your browser hitting the admin UI, your CLI — but external connections are dropped at the network level before they even reach Docker's routing.
The WAIAAS_DAEMON_HOSTNAME=0.0.0.0 environment variable controls what address the daemon process inside the container listens on (it needs to listen on all interfaces inside the container so Docker's internal networking can reach it). The 127.0.0.1: prefix on the port binding is what controls what's exposed outside the container to the host. These are two different layers, and understanding the distinction matters.
The Three-Layer Security Model
WAIaaS implements a 3-layer security model: session auth → time delay + approval → monitoring + kill switch. The localhost binding is your zeroth layer — the one that comes before any of this. If you skip it, you're relying entirely on application-level authentication to protect against the internet, which is not a position you want to be in.
Layer Zero: Network Isolation (You Configure This)
Keep the port bound to 127.0.0.1. If your AI agent runs on a different machine, use an SSH tunnel or a private network — not a public port.
Layer One: Authentication
WAIaaS uses three distinct authentication methods:
- masterAuth (Argon2id) — for system administration tasks like creating wallets and managing policies
- ownerAuth (SIWS/SIWE signatures) — for the fund owner to approve transactions or use the kill switch
- sessionAuth (JWT HS256) — for the AI agent to execute transactions and query balances
Your AI agent only ever needs the session token. It can't create wallets, can't modify policies, and can't approve its own pending transactions. These are structurally separated.
Layer Two: Policy Engine
Even with a valid session token, transactions aren't automatically approved. WAIaaS has 21 policy types and 4 security tiers (INSTANT, NOTIFY, DELAY, APPROVAL) with default-deny enforcement. A transaction is denied unless your policies explicitly allow it.
Here's a spending limit policy that enforces tiered approval:
curl -X POST http://127.0.0.1:3100/v1/policies \
-H "Content-Type: application/json" \
-H "X-Master-Password: my-secret-password" \
-d '{
"walletId": "<wallet-uuid>",
"type": "SPENDING_LIMIT",
"rules": {
"instant_max_usd": 100,
"notify_max_usd": 500,
"delay_max_usd": 2000,
"delay_seconds": 900,
"daily_limit_usd": 5000
}
}'
With this policy: transactions under $100 execute immediately, $100-$500 trigger a notification, $500-$2000 are delayed 15 minutes (giving you time to cancel), and anything above $2000 requires explicit owner approval. Your AI agent can't override these — they're enforced in the 7-stage transaction pipeline regardless of what the agent requests.
Deploying Securely: Step by Step
Here's the minimal secure deployment path.
Step 1: Clone and Configure
git clone https://github.com/waiaas/WAIaaS.git
cd WAIaaS
Don't modify the default port binding. Leave 127.0.0.1:3100:3100 as-is.
Step 2: Use Docker Secrets for Production
For production deployments, never put your master password in a plain environment variable or .env file. WAIaaS supports Docker Secrets:
# Create secret files
mkdir -p secrets
echo "your-secure-password" > secrets/master_password.txt
chmod 600 secrets/master_password.txt
# Deploy with secrets overlay
docker compose -f docker-compose.yml -f docker-compose.secrets.yml up -d
The docker-compose.secrets.yml overlay injects the password via Docker's secrets mechanism rather than environment variables, which means it doesn't appear in docker inspect output or process listings.
Step 3: Or Use Auto-Provision for Initial Setup
If you're starting fresh and want a zero-touch first run:
docker run -d \
--name waiaas \
-p 127.0.0.1:3100:3100 \
-v waiaas-data:/data \
-e WAIAAS_AUTO_PROVISION=true \
ghcr.io/waiaas/waiaas:latest
# Retrieve auto-generated master password
docker exec waiaas cat /data/recovery.key
WAIAAS_AUTO_PROVISION=true generates a random master password on first start and writes it to recovery.key. Once you've retrieved it, harden it with waiaas set-master and delete the recovery file.
Step 4: Verify It's Actually Localhost-Only
After starting the daemon, confirm the binding:
# Should show 127.0.0.1:3100, not 0.0.0.0:3100
docker compose ps
Then verify from a different machine (replace YOUR_SERVER_IP with your server's actual IP):
curl http://YOUR_SERVER_IP:3100/health
# Should timeout or refuse connection — that's correct behavior
If you can reach it from outside, check your port binding and your firewall rules.
Step 5: Create Wallets and Sessions
With the daemon running locally, use the CLI to set up:
npm install -g @waiaas/cli
waiaas quickset --mode mainnet
Or manually via the REST API:
# Create wallet
curl -X POST http://127.0.0.1:3100/v1/wallets \
-H "Content-Type: application/json" \
-H "X-Master-Password: my-secret-password" \
-d '{"name": "trading-wallet", "chain": "solana", "environment": "mainnet"}'
# Create session for your agent
curl -X POST http://127.0.0.1:3100/v1/sessions \
-H "Content-Type: application/json" \
-H "X-Master-Password: my-secret-password" \
-d '{"walletId": "<wallet-uuid>"}'
The session token you get back (wai_sess_...) is what your AI agent uses for all subsequent operations. It can check balances, send tokens, execute DeFi actions — but it's scoped to that wallet and constrained by whatever policies you've set.
What About the Admin UI and API Docs?
WAIaaS includes an Admin Web UI at /admin and interactive API docs at /reference (OpenAPI 3.0 spec at /doc). These are served by the same daemon on port 3100.
Because everything is on localhost, you access them from a browser on the same machine — or via an SSH tunnel:
# On your local machine, tunnel to the remote server
ssh -L 3100:127.0.0.1:3100 user@your-server
# Then open http://localhost:3100/admin in your browser
This is the correct pattern. You're not poking a hole in your firewall for a web interface — you're tunneling through an already-secured SSH connection.
The Non-Root Execution and Healthcheck
Two other security defaults worth noting in the WAIaaS Docker setup: the daemon runs as UID 1001 (non-root), and the Docker Compose configuration includes a healthcheck:
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:3100/health"]
interval: 30s
timeout: 5s
start_period: 10s
retries: 3
Running non-root limits blast radius if something goes wrong inside the container. The healthcheck ensures your orchestration layer (Docker, or whatever's managing restarts) knows when the daemon is actually ready versus just started. These aren't features you need to configure — they're shipped as defaults.
Key Environment Variables You Should Know
For a production deployment, these are the variables worth configuring explicitly:
WAIAAS_AUTO_PROVISION=true # Auto-generate master password on first start
WAIAAS_DAEMON_PORT=3100 # Listening port
WAIAAS_DAEMON_HOSTNAME=0.0.0.0 # Bind address (inside container)
WAIAAS_DAEMON_LOG_LEVEL=info # Log level (trace/debug/info/warn/error)
WAIAAS_DATA_DIR=/data # Data directory
WAIAAS_RPC_SOLANA_MAINNET=<url> # Solana mainnet RPC endpoint
WAIAAS_RPC_EVM_ETHEREUM_MAINNET=<url> # Ethereum mainnet RPC endpoint
Plugging in your own RPC endpoints (WAIAAS_RPC_SOLANA_MAINNET, WAIAAS_RPC_EVM_ETHEREUM_MAINNET) is worth doing for production — it removes dependency on shared public nodes and gives you better reliability and privacy for your agent's on-chain queries.
Common Pitfalls
Changing 127.0.0.1 to 0.0.0.0 in the port binding. Don't. If you need remote access, use an SSH tunnel or a reverse proxy with proper TLS and authentication in front.
Storing the master password in .env committed to version control. Use Docker Secrets or a secrets manager. The master password controls wallet creation, session management, and policy configuration — it's the most sensitive credential in the system.
Skipping policies because "my agent is trusted." Policies aren't about whether you trust your agent — they're about limiting the damage if the agent makes a mistake, gets a bad instruction, or is exploited. Default-deny exists for a reason.
Not verifying the port binding after deployment. It takes 30 seconds to confirm with docker compose ps. Do it every time you deploy to a new environment.
What's Next
The localhost binding is your foundation, but it's the starting point, not the complete picture. From here, explore WAIaaS's policy engine to configure exactly what your agent can and can't do — 21 policy types covering spending limits, token whitelists, contract restrictions, DeFi leverage caps, and more. For connecting AI frameworks like Claude to your self-hosted daemon, the MCP integration with 45 tools gives your agent full wallet capabilities over a local stdio transport, with no network exposure required.
The full source, Docker images, and documentation are at:
- GitHub: https://github.com/waiaas/WAIaaS
- Official site: https://waiaas.ai
Your keys, your server, your rules — and now your firewall is set up to keep it that way.
Top comments (0)