Docker Secrets for Private Keys: Production Security Best Practices
Docker Secrets for Private Keys: Production Security Best Practices for Self-Hosted AI Wallets
Would you trust a third party with your AI agent's private keys? If you're self-hosting your wallet infrastructure with Docker, how you handle secrets at runtime matters as much as where you host. This post walks through production-grade secret management for WAIaaS — the open-source, self-hosted Wallet-as-a-Service for AI agents — so your keys never appear in plaintext logs, environment variable dumps, or container inspection output.
Why Secret Management Actually Matters Here
There's a philosophical split happening in the AI agent infrastructure space. On one side, hosted services manage your agent's keys for you — convenient, but you're trusting a third party with custody. On the other side, self-hosting gives you full sovereignty: your keys, your server, your rules.
But self-hosting only delivers on that promise if you handle secrets correctly. The most common mistake is pasting a master password directly into a docker-compose.yml file or a .env file that ends up in version control. At that point, the threat model collapses — you've traded third-party custody risk for a Git history full of credentials. Docker Secrets, combined with WAIaaS's built-in secret management features, close that gap without requiring a dedicated secrets manager or HSM.
The stakes are real: the WAIaaS master password controls wallet creation, session issuance, and policy management. If it leaks, an attacker gets access to the full policy engine — including the ability to loosen or remove spending limits, whitelists, and approval requirements before your AI agent's next transaction.
How WAIaaS Handles Secrets
WAIaaS ships with two mechanisms designed specifically for production secret management: auto-provision and Docker Secrets support, both handled by the container entrypoint.
Auto-provision lets you start the daemon without specifying a master password at all. The entrypoint generates a cryptographically random password on first start and writes it to a recovery file inside the data volume:
docker run -d \
--name waiaas \
-p 127.0.0.1:3100:3100 \
-v waiaas-data:/data \
-e WAIAAS_AUTO_PROVISION=true \
ghcr.io/minhoyoo-iotrust/waiaas:latest
# Retrieve the auto-generated master password after first start
docker exec waiaas cat /data/recovery.key
This is the right starting point for a fresh deployment: no password ever appears in a compose file or environment variable. Once you've retrieved the recovery key, you can harden it with the CLI's set-master command and delete the recovery file.
Docker Secrets is the production path. Instead of passing the master password as an environment variable — which shows up in docker inspect, process listings, and anywhere the container runtime logs environment — you mount it as a file that only the entrypoint reads at startup.
Setting Up Docker Secrets the Right Way
Docker Secrets requires Docker Swarm mode for the native secrets: key in Compose, but WAIaaS also supports a simpler file-based overlay pattern that works with plain Docker Compose. Here's the production setup:
Step 1: Create Your Secret Files
mkdir -p secrets
echo "your-secure-master-password" > secrets/master_password.txt
chmod 600 secrets/master_password.txt
The chmod 600 matters — only the process owner should be able to read this file. Add secrets/ to your .gitignore immediately:
echo "secrets/" >> .gitignore
Step 2: Deploy with the Secrets Overlay
WAIaaS ships a docker-compose.secrets.yml overlay file designed for exactly this pattern. You layer it on top of the base compose file:
docker compose -f docker-compose.yml -f docker-compose.secrets.yml up -d
The overlay mounts your secret files into the container without them ever appearing as environment variables. The entrypoint reads the mounted file path at startup, loads the value into memory, and the plaintext never persists in the container's environment.
Step 3: Verify the Container Configuration
After deploying, confirm the master password isn't exposed in the container's environment:
docker inspect waiaas-daemon | grep -i password
With the secrets overlay in place, this should return nothing. The password exists only in the mounted file and in the daemon's memory after startup — not in any inspectable metadata.
The Base Docker Compose Configuration
For reference, here's the base docker-compose.yml that WAIaaS ships with:
services:
daemon:
image: ghcr.io/minhoyoo-iotrust/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 the port binding: 127.0.0.1:3100:3100. This is intentional — the daemon only listens on localhost, not 0.0.0.0. If you're running a reverse proxy in front (recommended for production), this prevents the API from being accidentally exposed on a public interface. The WAIaaS API runs on port 3100 by default.
Three-Layer Security After Deployment
Getting the master password into the container securely is the first layer. But WAIaaS's security model has two more layers that matter in production, and they work together with your secret management setup.
Layer 1: Authentication tiers. WAIaaS uses three distinct auth mechanisms. The master password (X-Master-Password) is for system administration: creating wallets, issuing sessions, and managing policies. Your AI agent never needs this — it uses a session token (Authorization: Bearer wai_sess_...). The owner can independently approve or reject transactions using wallet-based signatures (X-Owner-Signature). This separation means a compromised session token doesn't give an attacker the ability to create new wallets or change policies.
Layer 2: Policy engine with default-deny. Once you've created a wallet, the policy engine enforces what the session token is allowed to do. Critically, WAIaaS uses default-deny: if ALLOWED_TOKENS or CONTRACT_WHITELIST policies aren't configured, transactions are blocked. Here's a minimal spending limit policy to get started:
curl -X POST http://127.0.0.1:3100/v1/policies \
-H "Content-Type: application/json" \
-H "X-Master-Password: your-master-password" \
-d '{
"walletId": "<wallet-uuid>",
"type": "SPENDING_LIMIT",
"rules": {
"instant_max_usd": 10,
"notify_max_usd": 100,
"delay_max_usd": 1000,
"delay_seconds": 300,
"daily_limit_usd": 500,
"monthly_limit_usd": 5000
}
}'
The four security tiers — INSTANT, NOTIFY, DELAY, APPROVAL — mean that large transactions automatically queue for human review or require explicit owner approval, even if the session token is valid. WAIaaS supports 21 policy types total, covering everything from token whitelists and contract whitelists to perpetual futures leverage limits and DeFi lending LTV ratios.
Layer 3: Transaction monitoring and kill switch. The owner can approve or reject pending transactions via WalletConnect, and incoming transaction monitoring provides real-time deposit notifications. If something goes wrong, the owner auth gives you a kill switch independent of the master password.
Operational Security: What to Actually Do on Your Server
Beyond the Docker configuration, a few operational practices make a real difference for self-hosted deployments:
Restrict API access at the network level. The default port binding to 127.0.0.1 keeps the API local. If you need remote access, put a reverse proxy (nginx, Caddy) in front with TLS termination and authentication, rather than exposing port 3100 directly.
Use the dry-run API before live transactions. Before your AI agent runs live, test the full transaction flow with dryRun: true:
curl -X POST http://127.0.0.1:3100/v1/transactions/send \
-H "Content-Type: application/json" \
-H "Authorization: Bearer wai_sess_<token>" \
-d '{
"type": "TRANSFER",
"to": "recipient-address",
"amount": "0.1",
"dryRun": true
}'
This exercises the full 7-stage transaction pipeline — validation, auth, policy check, gas condition — without submitting anything on-chain. It's the right way to verify your policy configuration is working as intended before going live.
Monitor container health. The healthcheck in the compose file polls /health every 30 seconds. Wire this into whatever monitoring you already run on your homelab — Uptime Kuma, Prometheus, or even a simple cron job checking the endpoint.
Keep the image updated. The WAIaaS Docker image is ghcr.io/minhoyoo-iotrust/waiaas:latest. The compose file has restart: unless-stopped built in, and the Docker entrypoint supports Watchtower for automatic image updates if you prefer that approach.
Quick Start: Production Deployment in Five Steps
Here's the minimal path from zero to a production-ready self-hosted deployment:
Step 1: Clone and configure
git clone https://github.com/minhoyoo-iotrust/WAIaaS.git
cd WAIaaS
echo "secrets/" >> .gitignore
Step 2: Create secret files
mkdir -p secrets
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: Create a wallet and apply policies
# Create wallet
curl -X POST http://127.0.0.1:3100/v1/wallets \
-H "Content-Type: application/json" \
-H "X-Master-Password: $(cat secrets/master_password.txt)" \
-d '{"name": "agent-wallet", "chain": "solana", "environment": "mainnet"}'
# Apply a spending limit policy (use the wallet UUID from the response above)
curl -X POST http://127.0.0.1:3100/v1/policies \
-H "Content-Type: application/json" \
-H "X-Master-Password: $(cat secrets/master_password.txt)" \
-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
}
}'
Step 5: Create a session token for your AI agent
curl -X POST http://127.0.0.1:3100/v1/sessions \
-H "Content-Type: application/json" \
-H "X-Master-Password: $(cat secrets/master_password.txt)" \
-d '{"walletId": "<wallet-uuid>"}'
The session token you get back is what your AI agent uses. The master password stays on your server, in your secrets file, under your control.
The Self-Hosting Philosophy
Running your own wallet infrastructure is the crypto equivalent of running your own email server — except WAIaaS makes it considerably more practical. One Docker image, a compose file, and a secrets directory. No third party sees your keys, no hosted service has custody of your agent's funds, no rate limits from someone else's infrastructure.
The tradeoff is operational responsibility: you maintain the server, you manage the backup schedule (the CLI's backup create command handles this), and you own the recovery story if something goes wrong. But for developers who've already made the decision to self-host their homelab, none of that is unfamiliar territory.
What's different with AI agent wallets is the automation risk. An agent that can transact autonomously needs tighter guardrails than a human wallet, not looser ones. WAIaaS's policy engine — 21 policy types, default-deny enforcement, 4 security tiers — is designed specifically for that context.
What's Next
Explore the full WAIaaS documentation and source code at https://github.com/minhoyoo-iotrust/WAIaaS, and visit https://waiaas.ai for guides on connecting your self-hosted deployment to Claude Desktop via MCP, configuring DeFi policies for autonomous trading agents, and exploring the TypeScript and Python SDKs. If you're running WAIaaS in your homelab, the interactive API reference at http://127.0.0.1:3100/reference is the fastest way to explore all 39 route modules without leaving your local environment.