Skip to content

OpenClaw Centralized Secrets Setup

Architecture

M1 Max (openclaw host)
├── /Users/agent01/openclaw-docker/
│   ├── docker-compose.yml          ← mount secrets volume
│   ├── secrets/
│   │   └── .env                    ← SINGLE secrets file (gitignored)
│   ├── volumes/
│   │   ├── config/                 ← openclaw config
│   │   ├── workspace/              ← agent workspace + receipt_tools
│   │   └── storage/                ← vell-docs, receipts
│   └── .gitignore                  ← includes secrets/

AWS EC2 (mcp.vell.ai)
├── /opt/qbo-mcp-server/.env        ← QBO OAuth tokens (managed separately)
├── /opt/kajabi-mcp-server/.env      ← Kajabi creds (managed separately)
└── nginx                            ← reverse proxy: /mcp → QBO, /kajabi → Kajabi

Why centralize on the M1 Max?

The OpenClaw container runs scripts that need credentials for multiple services. Currently secrets are scattered across: - openclaw.json (MCP tokens) - Container env vars in docker-compose.yml - Hardcoded in individual scripts

A single secrets/.env file: - One place to audit, rotate, and back up - Mounted read-only into the container - Referenced by all scripts via OPENCLAW_SECRETS env var - Template (.env.example) is version-controlled; actual .env is not

Setup Steps

1. Create secrets directory on M1 Max

ssh openclaw-admin
mkdir -p /Users/agent01/openclaw-docker/secrets
cp openclaw-secrets.env.example /Users/agent01/openclaw-docker/secrets/.env
chmod 600 /Users/agent01/openclaw-docker/secrets/.env
echo "secrets/" >> /Users/agent01/openclaw-docker/.gitignore

2. Add docker-compose volume mount

Add to docker-compose.yml under the gateway service volumes:

services:
  gateway:
    # ... existing config ...
    volumes:
      # ... existing mounts ...
      - ./secrets/.env:/home/node/.secrets/.env:ro
    environment:
      - OPENCLAW_SECRETS=/home/node/.secrets/.env

3. Create AWS IAM user

# From your admin machine (M4 Pro) with AWS CLI configured
aws iam create-user --user-name openclaw-billing-reader

aws iam put-user-policy \
  --user-name openclaw-billing-reader \
  --policy-name BillingReadOnly \
  --policy-document file://aws-billing-readonly-policy.json

aws iam create-access-key --user-name openclaw-billing-reader
# Save AccessKeyId and SecretAccessKey → secrets/.env

4. Deploy the invoice downloader

# Copy script to OpenClaw workspace
scp aws-invoice-downloader.js openclaw:~/openclaw-docker/volumes/workspace/receipt_tools/

# Install AWS SDK deps in the container
ssh openclaw "DOCKER_HOST=unix:///Users/agent01/.colima/default/docker.sock \
  /opt/homebrew/bin/docker exec openclaw-docker-gateway-1 \
  npm install --prefix /home/node/openclaw/receipt_tools \
  @aws-sdk/client-invoicing @aws-sdk/client-cost-explorer dotenv"

5. Test

# Dry run from inside the container
ssh openclaw "DOCKER_HOST=unix:///Users/agent01/.colima/default/docker.sock \
  /opt/homebrew/bin/docker exec openclaw-docker-gateway-1 \
  node /home/node/openclaw/receipt_tools/aws-invoice-downloader.js --months 7 --dry-run"

6. Add to cron (via OpenClaw)

Tell the agent to add a monthly cron job:

"Add a monthly cron job on the 3rd at 9 AM CT to run aws-invoice-downloader.js for the previous month, then run match-and-attach.js --vendor AWS"

How scripts access secrets

// In any receipt_tools script:
import { config } from 'dotenv';
config({ path: process.env.OPENCLAW_SECRETS || '.env' });

// Now process.env.AWS_ACCESS_KEY_ID etc. are available

Adding new integrations

  1. Add the env vars to secrets/.env on the M1 Max
  2. Add the template vars to openclaw-secrets.env.example (committed)
  3. Restart the container: docker restart openclaw-docker-gateway-1
  4. Scripts pick up new vars automatically via OPENCLAW_SECRETS

Security notes

  • secrets/.env is chmod 600 (owner-only read/write)
  • Mounted :ro (read-only) in the container — agent can't modify it
  • Never commit secrets/.env — only the .env.example template
  • AWS IAM user has read-only billing access — no spend/modify permissions
  • Rotate keys quarterly: update secrets/.env, restart container