DEV Community

Cover image for Non-Root Docker Security: Running AI Agent Wallets as UID 1001
Wallet Guy
Wallet Guy

Posted on

Non-Root Docker Security: Running AI Agent Wallets as UID 1001

Non-Root Docker Security: Running AI Agent Wallets as UID 1001

Would you trust a third party with your AI agent's private keys? If you're running autonomous agents that move real money — swapping tokens, executing DeFi positions, paying for API calls — that's exactly what you're doing when you use a hosted wallet service. Self-hosting your wallet infrastructure means your keys live on your server, under your control, with no third party between your agent and its funds.

Why This Actually Matters

The conversation about self-custody usually focuses on humans holding their own keys. But the same logic applies to AI agents. If your agent is autonomously executing trades on Hyperliquid, staking on Lido, or paying for upstream API calls via x402, those private keys need to live somewhere. A hosted service means you're trusting that provider's security posture, their uptime, their rate limits, and their continued existence. Self-hosting means you own the full stack.

There's a deeper issue specific to Docker environments: running wallet processes as root inside a container is a well-documented security anti-pattern. If a vulnerability in any dependency allows container escape or privilege escalation, a root process is the worst-case scenario. The defense-in-depth answer is to run your wallet daemon as a non-root user with a fixed UID — which is exactly what WAIaaS does by default.

What WAIaaS Is

WAIaaS is an open-source, self-hosted Wallet-as-a-Service for AI agents. It's a 15-package monorepo that ships as a single Docker image, exposing a REST API with 39 route modules that your agents talk to directly. It handles everything from key management and transaction signing to policy enforcement and DeFi protocol execution — across 18 networks covering both Solana and EVM chains.

The Docker image runs as UID 1001 (non-root) out of the box. This isn't a config option you have to remember to set — it's the default. For self-hosters who've spent time hardening containers, this is the behavior you expect.

The Security Stack That Ships By Default

Before getting into the Docker specifics, it's worth understanding what you're deploying. WAIaaS has a 3-layer security model:

  1. Session authentication — AI agents authenticate with JWT tokens (sessionAuth). They can check balances and send transactions, but they can't create wallets or change policies.
  2. Time delay + approval — The policy engine has 4 security tiers: INSTANT, NOTIFY, DELAY, and APPROVAL. Large transactions can be held for human review.
  3. Monitoring + kill switch — Incoming transaction monitoring with real-time notifications, plus owner-controlled approval via WalletConnect or Telegram.

Authentication itself uses three distinct methods: masterAuth (Argon2id hashing) for system administration, ownerAuth (SIWS/SIWE signatures) for the fund owner, and sessionAuth (JWT HS256) for agents. Your agent only ever holds a session token — it can't touch the master password or create new wallets.

The policy engine enforces default-deny: if you haven't explicitly configured ALLOWED_TOKENS or CONTRACT_WHITELIST, transactions are blocked. This is the right default for a wallet that autonomous software is talking to.

Getting It Running

The fastest path is three commands:

git clone https://github.com/minhoyoo-iotrust/WAIaaS.git
cd WAIaaS
docker compose up -d
Enter fullscreen mode Exit fullscreen mode

That starts the daemon bound to 127.0.0.1:3100 — localhost only, not exposed to the internet. The default docker-compose.yml ships with a healthcheck:

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
Enter fullscreen mode Exit fullscreen mode

Notice the port binding: 127.0.0.1:3100:3100. The explicit loopback address means Docker won't accidentally expose this to your network even if your firewall rules have gaps. If you need external access, that's a deliberate change you make — not a surprise.

Auto-Provisioning for Unattended Setup

If you're deploying to a headless server and don't want to interactively set a master password, the entrypoint supports auto-provisioning:

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 auto-generated master password
docker exec waiaas cat /data/recovery.key
Enter fullscreen mode Exit fullscreen mode

WAIAAS_AUTO_PROVISION=true generates a random master password on first start and writes it to /data/recovery.key. Once you've retrieved it and stored it somewhere safe, you can harden it later with waiaas set-master. This is the practical path for homelab setups where you want automation without permanently keeping a weak password.

Production Secrets Without Environment Variable Leakage

Environment variables are convenient but they show up in docker inspect, process listings, and anywhere that reads /proc/<pid>/environ. For production deployments, WAIaaS ships with Docker Secrets support via a secrets overlay file:

# 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
Enter fullscreen mode Exit fullscreen mode

The docker-compose.secrets.yml overlay mounts your secret files into the container via Docker's native secrets mechanism rather than environment variables. This is the approach that passes security audits — secrets aren't visible in container metadata.

Creating Your First Wallet and Session

Once the daemon is running, the CLI (or direct API calls) handles the setup. Using the REST API directly:

# Create a wallet (masterAuth required)
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 a session token 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>"}'
Enter fullscreen mode Exit fullscreen mode

The session token (wai_sess_...) is what you hand to your agent. That token can check balances, send transactions, and execute DeFi actions — but it cannot create wallets, change policies, or access the master password. Principle of least privilege, applied.

Setting Spending Limits Before Letting Agents Loose

Before your agent touches mainnet funds, configure a spending policy. This is the guardrail that makes autonomous operation actually safe:

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
    }
  }'
Enter fullscreen mode Exit fullscreen mode

This creates four behavioral zones:

  • Under $100: execute immediately, no notification
  • $100–$500: execute immediately, send you a notification
  • $500–$2,000: queue for 15 minutes (you can cancel during that window)
  • Over $2,000: require your explicit approval before executing

The 21 available policy types cover everything from token whitelists to DeFi-specific limits like PERP_MAX_LEVERAGE and LENDING_LTV_LIMIT. For self-hosters who want to run a DeFi agent without constant supervision, these policies are the answer to "but what if the agent does something stupid."

Testing Before Real Execution

Before running any transaction in production, use the dry-run flag to simulate it:

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
  }'
Enter fullscreen mode Exit fullscreen mode

dryRun: true runs the full transaction pipeline — validation, policy checks, fee estimation — without broadcasting. You see exactly what would happen, including whether a policy would block it, before committing.

The CLI Quickstart Path

If you'd rather not curl your way through setup, the CLI covers the same ground:

npm install -g @waiaas/cli
waiaas init                        # Create data directory + config.toml
waiaas start                       # Start daemon
waiaas quickset --mode mainnet     # Create wallets + MCP sessions in one step
Enter fullscreen mode Exit fullscreen mode

quickset is the homelab-friendly shortcut — it creates wallets, generates session tokens, and prints the MCP configuration JSON you paste into Claude Desktop. The CLI has 20 commands total, including backup create and restore for the ops-minded among us.

Key Environment Variables Worth Knowing

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
Enter fullscreen mode Exit fullscreen mode

Setting your own RPC endpoints (WAIAAS_RPC_SOLANA_MAINNET, WAIAAS_RPC_EVM_ETHEREUM_MAINNET) is worth doing early. It removes the dependency on shared public RPC nodes, which have rate limits and occasionally go down. Running your own RPC or using a dedicated provider gives your agents consistent throughput.

The Philosophy in One Paragraph

Running your own email server used to be considered paranoid. Now, after enough high-profile provider breaches and unexpected service terminations, it looks like reasonable risk management. The same argument applies to AI agent wallets. Hosted services are convenient until they're not — until the provider changes their pricing, gets acquired, or has a security incident that affects your funds. Self-hosting means you took on the operational responsibility in exchange for full control. With WAIaaS running as UID 1001, Docker Secrets for credentials, default-deny policies, and a 3-layer auth model, the operational burden is manageable. Your keys, your server, your rules.

Quick Start Checklist

  1. Clone the repo and run docker compose up -d
  2. Set WAIAAS_AUTO_PROVISION=true for headless first-run, retrieve password from /data/recovery.key
  3. Create a wallet via the API or waiaas quickset
  4. Configure a SPENDING_LIMIT policy before giving session tokens to any agent
  5. For production, switch to the secrets overlay: docker compose -f docker-compose.yml -f docker-compose.secrets.yml up -d

What's Next

The full API reference auto-generates at http://127.0.0.1:3100/reference once your daemon is running — 39 route modules documented interactively via Scalar. For connecting Claude or other MCP-compatible agents to your self-hosted daemon, the waiaas mcp setup --all command handles the configuration automatically.


The source code, Dockerfiles, and full documentation are at github.com/minhoyoo-iotrust/WAIaaS. The project home is waiaas.ai.

Top comments (0)