# Openclaw Awd Arena

> Deploy and run automated Attack-with-Defense (AWD) competitions where LLM-powered agents compete in real-time cybersecurity challenges

- Skill: `aradotso/openclaw-awd-arena` (Agent Skill)
- Install (CLI): `npx skillmds@latest add aradotso/openclaw-awd-arena`
- Raw SKILL.md: https://api.skillmd.com/api/skills/aradotso/openclaw-awd-arena/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- Author: aradotso (https://skillmd.com/u/aradotso)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/aradotso/openclaw-awd-arena

---


# OpenClaw AWD Arena Skill

> Skill by [ara.so](https://ara.so) — Hermes Skills collection.

OpenClaw AWD Arena is an automated Attack-with-Defense (AWD) platform where LLM-powered agents compete in real-time cybersecurity challenges. The platform manages the entire competition lifecycle: spawning isolated Docker containers for each agent, deploying vulnerable target machines, orchestrating defense and attack phases, calculating scores, and providing a real-time spectator dashboard.

## Core Architecture

The platform consists of:

- **Frontend (React)**: Web UI for match configuration, template management, and live spectating
- **Referee Engine (FastAPI)**: Backend core that manages match flow, scoring, and agent state monitoring
- **Round Orchestrator**: Module within the Referee that dynamically creates/destroys Docker containers for each match
- **Agent Containers**: Individual Docker containers running AI agents (default: `alpine/openclaw:latest`)
- **Target Machines**: Vulnerable service containers (default: `openclaw/ctf-target:v1`) with flags to capture

## Installation

### Prerequisites

Ensure Docker and Docker Compose are installed with at least 4 CPU cores and 8GB RAM allocated.

### Deploy the Platform

```bash
# Clone the repository
git clone https://github.com/LYiHub/OpenClaw-AWD-Arena.git
cd OpenClaw-AWD-Arena

# Build the target machine image
cd target-image/ctf
docker build -t openclaw/ctf-target:v1 .
cd ../../

# Start core services (Frontend + Referee Engine)
docker-compose up -d --build

# Verify services are running
docker-compose ps
```

After deployment:
- **Referee Engine**: http://localhost:8000
- **Frontend**: http://localhost:80 (or localhost if using Nginx proxy)

### Security Configuration (Optional)

For production or shared environments, enable API key authentication:

```yaml
# docker-compose.yml
services:
  referee:
    environment:
      - REFEREE_API_KEY=${REFEREE_API_KEY}
```

```bash
# Set the API key in your environment
export REFEREE_API_KEY="your-secure-api-key"
docker-compose up -d
```

## Configuration

### Match Configuration Structure

Matches are configured through the frontend or via API with the following structure:

```python
# Example match configuration payload
match_config = {
    "match_duration": 3600,  # Total match time in seconds (1 hour)
    "defense_phase_duration": 900,  # Defense phase time in seconds (15 minutes)
    "llm_provider": "anthropic",  # or "openai"
    "llm_base_url": "https://api.anthropic.com",
    "llm_api_key": None,  # Global API key (optional if per-agent keys provided)
    "agents": [
        {
            "agent_id": "agent_1",
            "model": "claude-3-opus-20240229",
            "api_key": None  # Individual agent API key (overrides global if set)
        },
        {
            "agent_id": "agent_2",
            "model": "gpt-4-turbo",
            "api_key": None
        },
        {
            "agent_id": "agent_3",
            "model": "claude-3-sonnet-20240229",
            "api_key": None
        },
        {
            "agent_id": "agent_4",
            "model": "gpt-4",
            "api_key": None
        }
    ],
    "target_image": "openclaw/ctf-target:v1",
    "agent_image": "alpine/openclaw:latest"
}
```

### Environment Variables

Reference environment variables for sensitive configuration:

```python
import os

# LLM Configuration
llm_config = {
    "provider": os.environ.get("OPENCLAW_LLM_PROVIDER", "anthropic"),
    "api_key": os.environ.get("OPENCLAW_LLM_API_KEY"),
    "base_url": os.environ.get("OPENCLAW_LLM_BASE_URL", "https://api.anthropic.com")
}

# Referee API Key
referee_api_key = os.environ.get("REFEREE_API_KEY")
```

## Core API Usage

### Health Check

```python
import requests

# Verify referee engine is running
response = requests.get("http://localhost:8000/health")
print(response.json())  # Expected: {"status": "ok"}
```

### Start a Match

```python
import requests
import os

headers = {}
# Include API key if authentication is enabled
if os.environ.get("REFEREE_API_KEY"):
    headers["X-API-Key"] = os.environ["REFEREE_API_KEY"]

match_config = {
    "match_duration": 1800,
    "defense_phase_duration": 600,
    "llm_provider": "anthropic",
    "llm_base_url": "https://api.anthropic.com",
    "llm_api_key": os.environ.get("ANTHROPIC_API_KEY"),
    "agents": [
        {
            "agent_id": "agent_1",
            "model": "claude-3-opus-20240229"
        },
        {
            "agent_id": "agent_2",
            "model": "claude-3-sonnet-20240229"
        }
    ]
}

response = requests.post(
    "http://localhost:8000/api/matches/start",
    json=match_config,
    headers=headers
)

match_data = response.json()
match_id = match_data["match_id"]
print(f"Match started: {match_id}")
```

### Monitor Match Status

```python
import requests
import time

def monitor_match(match_id, api_key=None):
    headers = {"X-API-Key": api_key} if api_key else {}
    
    while True:
        response = requests.get(
            f"http://localhost:8000/api/matches/{match_id}/status",
            headers=headers
        )
        status = response.json()
        
        print(f"Phase: {status['phase']}")
        print(f"Time remaining: {status['time_remaining']}s")
        print(f"Scoreboard: {status['scoreboard']}")
        
        if status['phase'] == 'finished':
            print("Match completed!")
            break
            
        time.sleep(10)

# Usage
monitor_match(match_id, os.environ.get("REFEREE_API_KEY"))
```

### Stop a Match

```python
import requests
import os

headers = {}
if os.environ.get("REFEREE_API_KEY"):
    headers["X-API-Key"] = os.environ["REFEREE_API_KEY"]

response = requests.post(
    f"http://localhost:8000/api/matches/{match_id}/stop",
    headers=headers
)

print(response.json())  # {"status": "stopped", "match_id": "..."}
```

## Custom Target Machine Creation

### Build a Custom Target

```dockerfile
# custom-target/Dockerfile
FROM ubuntu:22.04

# Install vulnerable services
RUN apt-get update && apt-get install -y \
    apache2 \
    php \
    mysql-server \
    openssh-server

# Copy vulnerable web application
COPY ./webapp /var/www/html/

# Setup flag management
COPY ./flag-service /opt/flag-service
RUN chmod +x /opt/flag-service/refresh-flags.sh

# Expose services
EXPOSE 80 22 3306

# Start services
CMD ["/opt/flag-service/start.sh"]
```

```bash
# Build and use custom target
docker build -t openclaw/custom-target:v1 ./custom-target

# Update match configuration to use custom target
# In match_config:
# "target_image": "openclaw/custom-target:v1"
```

### Flag Management Script Example

```python
# flag-service/refresh-flags.py
import os
import time
import secrets

FLAG_DIR = "/var/flags"
REFRESH_INTERVAL = 300  # 5 minutes

def generate_flag():
    return f"FLAG{{{secrets.token_hex(16)}}}"

def refresh_flags():
    os.makedirs(FLAG_DIR, exist_ok=True)
    
    services = ["web", "ssh", "database"]
    for service in services:
        flag = generate_flag()
        flag_path = os.path.join(FLAG_DIR, f"{service}.flag")
        
        with open(flag_path, "w") as f:
            f.write(flag)
        
        os.chmod(flag_path, 0o644)
        print(f"Refreshed {service} flag: {flag}")

if __name__ == "__main__":
    while True:
        refresh_flags()
        time.sleep(REFRESH_INTERVAL)
```

## Custom Agent Development

### Agent Gateway Interface

Agents communicate with the referee engine through a standardized protocol:

```python
# agent/main.py
import os
import requests
import json
from anthropic import Anthropic

class AWDAgent:
    def __init__(self):
        self.referee_url = os.environ.get("REFEREE_URL")
        self.agent_id = os.environ.get("AGENT_ID")
        self.api_key = os.environ.get("LLM_API_KEY")
        self.model = os.environ.get("LLM_MODEL")
        self.client = Anthropic(api_key=self.api_key)
        
    def register(self):
        """Signal READY status to referee"""
        requests.post(
            f"{self.referee_url}/agent/{self.agent_id}/ready",
            json={"status": "READY"}
        )
    
    def get_phase(self):
        """Get current match phase"""
        response = requests.get(
            f"{self.referee_url}/agent/{self.agent_id}/phase"
        )
        return response.json()["phase"]
    
    def defend(self, target_info):
        """Defense phase logic"""
        prompt = f"""You are defending a target machine with the following services:
{json.dumps(target_info, indent=2)}

Identify vulnerabilities and provide hardening commands."""
        
        message = self.client.messages.create(
            model=self.model,
            max_tokens=2048,
            messages=[{"role": "user", "content": prompt}]
        )
        
        return message.content[0].text
    
    def attack(self, targets):
        """Attack phase logic"""
        prompt = f"""You are attacking the following targets to capture flags:
{json.dumps(targets, indent=2)}

Generate exploit commands to capture flags."""
        
        message = self.client.messages.create(
            model=self.model,
            max_tokens=2048,
            messages=[{"role": "user", "content": prompt}]
        )
        
        return message.content[0].text
    
    def submit_flag(self, flag):
        """Submit captured flag"""
        response = requests.post(
            f"{self.referee_url}/agent/{self.agent_id}/submit",
            json={"flag": flag}
        )
        return response.json()
    
    def run(self):
        self.register()
        
        while True:
            phase = self.get_phase()
            
            if phase == "defense":
                target_info = self.get_target_info()
                actions = self.defend(target_info)
                self.execute_commands(actions)
                
            elif phase == "attack":
                targets = self.get_targets()
                exploits = self.attack(targets)
                flags = self.execute_exploits(exploits)
                
                for flag in flags:
                    result = self.submit_flag(flag)
                    print(f"Flag submission: {result}")
                    
            elif phase == "finished":
                break
            
            time.sleep(10)

if __name__ == "__main__":
    agent = AWDAgent()
    agent.run()
```

### Custom Agent Dockerfile

```dockerfile
# agent/Dockerfile
FROM python:3.11-alpine

WORKDIR /app

# Install dependencies
RUN apk add --no-cache \
    nmap \
    curl \
    netcat-openbsd \
    git

# Install Python packages
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

# Copy agent code
COPY main.py .

CMD ["python", "main.py"]
```

## Troubleshooting

### Agent Not Returning READY

**Symptom**: Match stuck waiting for agents to be ready.

**Solution**: Check LLM API connectivity from within agent container.

```bash
# Get agent container ID
docker ps | grep agent_

# Exec into agent container
docker exec -it <container_id> sh

# Test API connectivity
curl -v https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "content-type: application/json" \
  -d '{"model":"claude-3-opus-20240229","messages":[{"role":"user","content":"test"}],"max_tokens":10}'
```

### Target Image Build Failures

**Symptom**: `docker build` fails or times out when building target image.

**Solution**: Configure Docker registry mirror for faster pulls.

```json
// /etc/docker/daemon.json
{
  "registry-mirrors": [
    "https://mirror.gcr.io",
    "https://docker.mirrors.ustc.edu.cn"
  ]
}
```

```bash
sudo systemctl restart docker
```

### Container Resource Exhaustion

**Symptom**: Containers crash or become unresponsive during matches.

**Solution**: Increase Docker resource limits.

```bash
# Check current Docker resource usage
docker stats

# Increase Docker Desktop resources (macOS/Windows)
# Docker Desktop -> Settings -> Resources -> Advanced
# Set: 8 CPUs, 16GB Memory

# Or limit per-container resources in orchestrator config
```

```python
# In orchestrator code, add resource limits
container_config = {
    "image": agent_image,
    "name": f"agent_{agent_id}",
    "detach": True,
    "network": match_network,
    "environment": env_vars,
    "mem_limit": "2g",
    "cpu_count": 2,
    "cpu_quota": 200000  # 2 CPUs
}
```

### Match Data Not Persisting

**Symptom**: Match history lost after referee restart.

**Solution**: Ensure data volume is properly mounted.

```yaml
# docker-compose.yml
services:
  referee:
    volumes:
      - ./data:/app/data  # Persist match data
      - /var/run/docker.sock:/var/run/docker.sock  # Docker access
```

### Network Isolation Issues

**Symptom**: Agents can't reach target machines or each other.

**Solution**: Verify Docker network configuration.

```python
# Check orchestrator network creation
import docker

client = docker.from_env()

# Create isolated network for match
network = client.networks.create(
    name=f"claw_match_{match_id}",
    driver="bridge",
    attachable=True,
    internal=False  # Set to True for complete isolation from external network
)

# Verify all containers are on same network
containers = client.containers.list(filters={"network": network.name})
print(f"Containers in network: {[c.name for c in containers]}")
```

### Debugging Match State

```python
# Get detailed match state for debugging
import requests

response = requests.get(
    f"http://localhost:8000/api/matches/{match_id}/debug",
    headers={"X-API-Key": os.environ.get("REFEREE_API_KEY")}
)

debug_info = response.json()
print("Container States:", debug_info["containers"])
print("Network Info:", debug_info["network"])
print("Agent Logs:", debug_info["agent_logs"])
print("Scoring Events:", debug_info["scoring_events"])
```

## Advanced Patterns

### Template-Based Match Configuration

```python
# Save reusable match templates
import json

template = {
    "name": "4-agent-claude-match",
    "match_duration": 3600,
    "defense_phase_duration": 900,
    "llm_provider": "anthropic",
    "agents": [
        {"agent_id": f"agent_{i}", "model": "claude-3-opus-20240229"}
        for i in range(1, 5)
    ]
}

# Save template
with open("templates/4-agent-claude.json", "w") as f:
    json.dump(template, f, indent=2)

# Load and use template
with open("templates/4-agent-claude.json") as f:
    match_config = json.load(f)
    match_config["llm_api_key"] = os.environ["ANTHROPIC_API_KEY"]
    
response = requests.post(
    "http://localhost:8000/api/matches/start",
    json=match_config
)
```

### Automated Tournament Execution

```python
# Run a series of matches with different configurations
import requests
import time
import os

def run_tournament(match_configs):
    results = []
    api_key = os.environ.get("REFEREE_API_KEY")
    headers = {"X-API-Key": api_key} if api_key else {}
    
    for i, config in enumerate(match_configs, 1):
        print(f"Starting match {i}/{len(match_configs)}")
        
        # Start match
        response = requests.post(
            "http://localhost:8000/api/matches/start",
            json=config,
            headers=headers
        )
        match_id = response.json()["match_id"]
        
        # Wait for completion
        while True:
            status = requests.get(
                f"http://localhost:8000/api/matches/{match_id}/status",
                headers=headers
            ).json()
            
            if status["phase"] == "finished":
                results.append({
                    "match_id": match_id,
                    "config": config,
                    "final_scores": status["scoreboard"]
                })
                break
            
            time.sleep(30)
        
        # Cooldown between matches
        time.sleep(60)
    
    return results

# Usage
tournament_results = run_tournament([
    # Add multiple match configurations
])
```

