Securing a 15-Package Crypto Monorepo: Production Docker Secrets Best Practices
Would you trust a third party with your AI agent's private keys? If you're running a self-hosted crypto wallet infrastructure for AI agents, how you handle secrets in Docker can be the difference between a hardened production setup and a disaster waiting to happen. This post walks through the production Docker secrets approach used in WAIaaS — an open-source, self-hosted Wallet-as-a-Service for AI agents — and why getting this right matters more than most developers realize.
Why Secrets Management Is Not an Afterthought
Here's the uncomfortable truth about Docker deployments: the naive approach of passing sensitive values through environment variables directly in your compose file is everywhere, and it's a real risk. If you've ever done docker inspect on a running container, you know those environment variables show up in plain text. If you use a CI/CD system that logs environment variables, they show up there too. And if someone gets read access to your Docker daemon socket — which happens more than people admit — they can pull every secret from every running container.
For a crypto wallet backend like WAIaaS, the stakes are concrete. The master password (authenticated via Argon2id) is what protects access to wallet creation, session management, and policy enforcement. The session tokens that AI agents carry are the keys to executing transactions. The owner signatures are what stand between your funds and an unauthorized actor. None of these should ever appear in a docker-compose.yml file committed to a git repository, logged to stdout, or visible in docker inspect output.
WAIaaS is a 15-package monorepo — actions, adapters, admin, cli, core, daemon, desktop-spike, e2e-tests, mcp, openclaw-plugin, push-relay, sdk, shared, skills, and wallet-sdk. That scope means multiple packages touching sensitive credentials, and a single careless secret exposure can cascade. The good news: the project ships with a production secrets pattern that solves this cleanly.
The Two-File Compose Pattern
WAIaaS uses a two-file Docker Compose approach for production secrets. The base docker-compose.yml defines the service configuration without any secrets baked in, and a separate docker-compose.secrets.yml overlays secret file mounts for production. You never commit the secrets overlay with actual values — just the structure.
Here's the base setup:
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 a few things here already:
Port binding is
127.0.0.1:3100:3100— not0.0.0.0:3100:3100. This means the daemon only accepts connections on localhost by default. If you're on a VPS, this alone prevents your wallet API from being exposed to the open internet. You add a reverse proxy in front of it with TLS termination. Simple, effective.env_filewithrequired: false— this means a.envfile is optional. You can add it to your.gitignoreand it won't break the setup if it's absent.Named volume
waiaas-data— your wallet data persists in a named Docker volume, not bind-mounted to a path that might have wrong permissions or show up in unexpected places.
Now for the secrets overlay:
# 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 chmod 600 step matters. Docker reads these files as the daemon user, but you want to make sure no other process on the host can read them. The secrets directory should also not be inside your project's git repository — add it to .gitignore immediately.
When you deploy with the overlay, Docker mounts the secret files into the container at specific paths (like /run/secrets/master_password). The entrypoint script reads from those paths rather than from environment variables. This means the secret never appears as an environment variable inside the container, and docker inspect won't reveal it.
Auto-Provision for First-Time Setup
For first-time setup or development environments, WAIaaS supports an auto-provision mode that generates the master password for you:
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
The WAIAAS_AUTO_PROVISION=true flag tells the entrypoint to generate a random master password on first start and write it to /data/recovery.key inside the data volume. You then read it once, store it in your secrets manager or password manager, and optionally harden it later.
This is the self-hosted equivalent of the "temporary password" pattern you see in database containers — except this one is for your crypto wallet infrastructure. The critical step is that you rotate it. After first boot, retrieve the recovery key, then use the CLI to set a proper master password:
npm install -g @waiaas/cli
waiaas set-master
Once you've set a hardened password and stored it properly, delete the recovery key file. Don't leave it sitting in the volume indefinitely.
The Non-Root Container
One detail worth calling out: the WAIaaS Docker image runs as a non-root user (UID 1001). This is a meaningful security boundary. If something goes wrong and an attacker gains code execution inside the container, they're running as an unprivileged user rather than root. Combined with the 127.0.0.1 port binding, this significantly limits the blast radius of a container escape.
For your data volume, this means you need to make sure the mount point is writable by UID 1001. With named Docker volumes, Docker handles this automatically. If you switch to bind mounts (e.g., -v /home/user/waiaas-data:/data), you need to chown 1001:1001 /home/user/waiaas-data on the host before starting the container.
Environment Variables Reference
The key environment variables you'll configure for a production deployment:
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
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
Your RPC endpoints are also secrets, in a sense — or at least, rate-limited credentials you don't want to share. Put your RPC URLs in the .env file (which is .gitignore'd) or in the secrets overlay, not hardcoded in the base compose file.
Notice that WAIAAS_DAEMON_HOSTNAME=0.0.0.0 in the base compose file sets the internal bind address to all interfaces within the container, which is correct — the container-level port binding (127.0.0.1:3100:3100) is what restricts external access, not the internal hostname.
The Three Auth Layers
Even with perfect Docker secrets management, you want to understand what you're protecting. WAIaaS uses three distinct authentication methods, each for a different principal:
# masterAuth — system administrator (wallet creation, session management, policies)
-H "X-Master-Password: my-secret-password"
# sessionAuth — AI agent (transactions, balance queries, DeFi actions)
-H "Authorization: Bearer wai_sess_eyJhbGciOiJIUzI1NiJ9..."
# ownerAuth — fund owner (transaction approval, kill switch recovery)
-H "X-Owner-Signature: <ed25519-or-secp256k1-signature>"
-H "X-Owner-Message: <signed-message>"
The master password (protected by Argon2id) is what your Docker secrets setup is primarily protecting. An AI agent never needs it — agents only hold session tokens (JWT HS256) with per-session TTL, max renewals, and absolute lifetime limits. The owner signature is derived from your own crypto wallet, which never touches the server at all.
This separation means even if a session token leaks, the attacker can only do what that session is authorized to do — and the policy engine enforces limits on that. The policy engine uses 21 policy types with 4 security tiers (INSTANT, NOTIFY, DELAY, APPROVAL) and default-deny enforcement: transactions are blocked unless explicitly allowed. A leaked session token against a properly configured policy is far less catastrophic than a leaked master password.
Quick Production Deployment
Here's the complete self-hosted setup from scratch:
Step 1: Clone and configure
git clone https://github.com/waiaas/WAIaaS.git
cd WAIaaS
echo "secrets/" >> .gitignore
mkdir -p secrets
Step 2: Create your secret files
echo "$(openssl rand -base64 32)" > secrets/master_password.txt
chmod 600 secrets/master_password.txt
Step 3: Start with secrets overlay
docker compose -f docker-compose.yml -f docker-compose.secrets.yml up -d
Step 4: Verify the daemon is healthy
docker compose logs -f
# Wait for healthcheck to pass, then:
curl http://127.0.0.1:3100/health
Step 5: Create your first wallet and session
npm install -g @waiaas/cli
waiaas init
waiaas quickset --mode mainnet
The quickset command creates wallets and MCP sessions in one step — you'll get the MCP configuration JSON to paste into Claude Desktop, giving your AI agent wallet access through 45 MCP tools.
Watchtower for Automatic Updates
One practical self-hosting concern: keeping the container updated without manual intervention. The WAIaaS setup supports Watchtower for automatic image updates. You add a Watchtower container to your compose stack, point it at the WAIaaS daemon container, and it will pull and restart with the latest image when updates are available.
This is the self-hosted answer to "how do I get security patches without thinking about it." For a crypto wallet backend, staying on the latest image matters — the codebase has 684+ test files and active development, and security fixes get shipped in image updates.
What This Gives You
Running WAIaaS self-hosted means:
- No third-party custody — your wallet private keys never leave your server
- No API rate limits from a hosted service — you're calling your own daemon
- Full auditability — it's open source, all 15 packages, you can read every line
- Network sovereignty — 18 networks across EVM and Solana, configured with your own RPC endpoints
- Policy control — you define the 21 policy types that govern what AI agents can do with your funds
The Docker secrets pattern described here is the difference between "self-hosted in theory" and "self-hosted in practice." Getting your .gitignore right, using the secrets overlay, binding to localhost, and rotating the auto-provisioned password — these are the steps that make it real.
What's Next
The best place to go deeper is the WAIaaS GitHub repository at https://github.com/waiaas/WAIaaS, where you can read the entrypoint script, inspect the Dockerfiles, and explore the full policy engine implementation. For the broader project context, including the MCP integration and SDK documentation, visit https://waiaas.ai. If you're already running the daemon, the interactive API reference at http://127.0.0.1:3100/reference gives you a live Scalar UI against your own instance — no external service, no account required.
Top comments (0)