Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions .github/workflows/gatekeeper-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
name: gatekeeper-image

on:
push:
branches: [main]
paths: [gatekeeper/**]
pull_request:
paths: [gatekeeper/**]

env:
IMAGE: ghcr.io/${{ github.repository_owner }}/openclaw-gatekeeper

jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- uses: actions/checkout@v4

- uses: docker/login-action@v3
if: github.event_name == 'push'
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/build-push-action@v5
with:
context: gatekeeper
push: ${{ github.event_name == 'push' }}
tags: |
${{ env.IMAGE }}:latest
${{ env.IMAGE }}:${{ github.sha }}
165 changes: 165 additions & 0 deletions docs/gatekeeper.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
# Gatekeeper Sidecar Design

## Problem

OpenClaw is an AI agent gateway. The agent can execute arbitrary tools — bash commands, Python scripts, HTTP calls — as part of its reasoning loop. This creates a fundamental tension:

> The agent needs secrets (API tokens, bot keys) to function, but giving the agent access to its own secrets means a compromised or misbehaving agent can exfiltrate them.

Current risk with naive approaches:

- Secrets in env vars → agent reads `process.env` or `/proc/self/environ`
- Secrets in K8s Secret mounted as files → agent reads the file directly
- Secrets in K8s Secret as env vars → same problem
- No audit trail → you don't know when or why a secret was accessed

The core problem: **the agent and its secrets live in the same trust boundary.**

## Goal

Move secrets out of the agent's trust boundary entirely, while still allowing the agent to function — with a human approval gate on every secret access.

## Overview

Gatekeeper is a sidecar container that runs alongside the OpenClaw main container within the same Kubernetes pod. It acts as the sole holder of secrets, enforcing human-in-the-loop approval via Telegram before returning any secret to OpenClaw.

OpenClaw itself holds **zero secrets** — no env vars, no files, no keys.

## Architecture

```
┌─────────────────── K3s / K8s Cluster ─────────────────────────┐
│ │
│ ┌── AWS Secrets Manager ──┐ │
│ │ openclaw/tokens │ │
│ │ TELEGRAM_TOKEN_1 │ │
│ │ TELEGRAM_TOKEN_2 │ │
│ │ GATEWAY_TOKEN │ │
│ └────────────┬─────────────┘ │
│ │ IAM Role (sidecar SA only) │
│ ▼ │
│ ┌──────────────────── OpenClaw Pod ────────────────────────┐ │
│ │ │ │
│ │ ┌─────────────────────┐ ┌──────────────────────┐ │ │
│ │ │ main (OpenClaw) │ │ gatekeeper (sidecar) │ │ │
│ │ │ │ │ │ │ │
│ │ │ ❌ no secrets │ │ ✅ IAM Role │ │ │
│ │ │ ❌ no env keys │ │ ✅ fetches from AWS │ │ │
│ │ │ ❌ no AWS access │ │ ✅ Telegram approval │ │ │
│ │ │ │ │ ✅ rate limiting │ │ │
│ │ │ exec: curl unix ───────► /tmp/gatekeeper.sock │ │ │
│ │ │ socket → get secret│ │ │ │ │ │
│ │ │ │ │ ▼ │ │ │
│ │ │ │ │ 📱 Telegram notify │ │ │
│ │ │ │ │ [✅ Approve][❌ Deny]│ │ │
│ │ │ │ │ │ │ │ │
│ │ │ ◄── secret returned────────────── ▼ │ │ │
│ │ │ (stored in memory) │ │ fetch from AWS SM │ │ │
│ │ └─────────────────────┘ └──────────────────────┘ │ │
│ │ shared emptyDir volume (/tmp/gatekeeper.sock) │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────┘

📱 Your Phone
┌──────────────────────┐
│ 🔐 Gatekeeper Alert │
│ │
│ OpenClaw requests │
│ secret access │
│ Time: 09:58 │
│ │
│ [✅ Approve][❌ Deny] │
└──────────────────────┘
```

## Security Model

| Threat | Mitigation |
|--------|-----------|
| Agent reads env vars | No secrets in main container env |
| Agent reads sidecar filesystem | Different container — filesystem isolated |
| Agent calls unix socket directly | Sidecar sends Telegram alert — you deny |
| Agent runs `aws secretsmanager get-secret-value` | Main container SA has no IAM permissions |
| `kubectl exec` into sidecar | Requires cluster RBAC — agent doesn't have it |
| Brute-force socket requests | Rate limit: max 1 request per 5 minutes; alerts on repeated attempts |

## Components

### 1. AWS Secrets Manager
- Stores all tokens and keys under a single path (e.g. `openclaw/tokens`)
- Supports automatic rotation
- Every access is logged in CloudTrail

### 2. Gatekeeper Sidecar
- Minimal container (Alpine-based)
- Listens on `/tmp/gatekeeper.sock` (shared `emptyDir` volume)
- On request:
1. Sends Telegram approval message with Approve / Deny buttons
2. Waits up to `approvalTimeoutSeconds` for response
3. On approval: fetches secret from AWS Secrets Manager via IAM Role, returns to caller
4. On denial or timeout: returns error
- Rate limit: configurable, default 1 request per 5 minutes
- Alerts on anomalous repeated requests

### 3. IAM / RBAC Separation
- Gatekeeper uses a dedicated `ServiceAccount` annotated with an IAM Role (IRSA or EKS Pod Identity)
- Main container uses a separate `ServiceAccount` with **no AWS permissions**
- RBAC: neither SA has `exec` rights into pods

## Helm Values

```yaml
gatekeeper:
enabled: false # opt-in
image:
repository: ghcr.io/thepagent/openclaw-gatekeeper
tag: latest
aws:
region: ap-northeast-1
secretsManagerPath: openclaw/tokens
telegram:
approvalTimeoutSeconds: 60
rateLimitMinutes: 5
serviceAccount:
annotations:
eks.amazonaws.com/role-arn: arn:aws:iam::ACCOUNT_ID:role/openclaw-gatekeeper
```

## Repository Layout

```
openclaw-helm/
├── gatekeeper/ # sidecar source code
│ ├── Dockerfile
│ ├── main.py (or main.go)
│ └── requirements.txt
├── templates/
│ ├── deployment.yaml # injects sidecar when gatekeeper.enabled=true
│ └── serviceaccount.yaml # separate SA for gatekeeper
├── docs/
│ └── gatekeeper.md # this document
└── .github/workflows/
└── gatekeeper-image.yml # CI: build & push gatekeeper image on change
```

## CI / CD

A GitHub Actions workflow (`gatekeeper-image.yml`) will:
1. Trigger on changes to `gatekeeper/**`
2. Build the Docker image
3. Push to `ghcr.io/thepagent/openclaw-gatekeeper` with the commit SHA tag and `latest`

## Deployment Flow

1. Create IAM Role with `secretsmanager:GetSecretValue` on `openclaw/tokens`
2. Store secrets in AWS Secrets Manager
3. Set `gatekeeper.enabled: true` and configure `values.yaml`
4. `helm upgrade --install openclaw oci://ghcr.io/thepagent/openclaw-helm -f values.yaml`
5. On first OpenClaw startup, approve the Telegram request on your phone

## Future Considerations

- **Audit log**: persist approval/denial events to a local file or CloudWatch
- **Multi-secret support**: allow OpenClaw to request individual named secrets rather than the full bundle
- **mTLS over socket**: replace plain unix socket with mTLS for stronger channel integrity
21 changes: 21 additions & 0 deletions gatekeeper/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
[package]
name = "gatekeeper"
version = "0.1.0"
edition = "2021"

[dependencies]
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
aws-config = { version = "1", features = ["behavior-version-latest"] }
aws-sdk-secretsmanager = "1"
reqwest = { version = "0.12", features = ["json"] }
zeroize = { version = "1", features = ["derive"] }
tracing = "0.1"
tracing-subscriber = "0.3"
anyhow = "1"

[profile.release]
opt-level = "z"
lto = true
strip = true
11 changes: 11 additions & 0 deletions gatekeeper/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
FROM rust:1.77-alpine AS builder
RUN apk add --no-cache musl-dev pkgconfig openssl-dev
WORKDIR /build
COPY Cargo.toml Cargo.lock ./
COPY src ./src
RUN cargo build --release

FROM alpine:3.19
RUN apk add --no-cache ca-certificates
COPY --from=builder /build/target/release/gatekeeper /usr/local/bin/gatekeeper
ENTRYPOINT ["/usr/local/bin/gatekeeper"]
Loading
Loading