Metadata-Version: 2.3
Name: breslin
Version: 0.1.0
Summary: Any break requires three things: knowing the layout, understanding the routine and help from outside or inside
Author: vvcb
Author-email: vvcb <vvcb.n1@gmail.com>
Requires-Dist: deepagents>=0.6.8
Requires-Dist: fastmcp>=3.4.0
Requires-Python: >=3.14
Description-Content-Type: text/markdown

# Breslin

An **authorised** red-team CTF agent for Kubernetes. It runs
[opencode](https://github.com/sst/opencode) in headless mode inside a
deliberately-constrained pod and attempts a catalog of read-only
enumeration / credential-access / lateral-movement techniques against the
cluster, validating that the platform's security controls (RBAC,
NetworkPolicy, Pod Security Admission) actually hold.

The agent runs as a one-shot Kubernetes **Job**: it captures planted
`KARECTL{...}` flags where controls are weak, and records `BLOCKED` where they
hold. A blocked attempt is as valuable as a capture.

> ⚠️ This is a sanctioned security-validation tool. It is read-only by design
> (no `delete` / `patch` / `create` against live objects) and is scoped to a
> sandbox namespace via namespace-only RBAC. Only run it against clusters you
> own or are explicitly authorised to test.

---

## Repository layout

```
.
├── Dockerfile              # container image (system tools + kubectl + opencode + uv venv)
├── pyproject.toml          # Python project (uv-managed); console script: redteam-agent
├── uv.lock
├── src/redteam_agent/      # the Python wrapper (was entrypoint.sh)
│   ├── cli.py              #   orchestration + opencode launch
│   ├── config.py           #   backend config + opencode config writer
│   └── preflight.py        #   identity canary + backend reachability
├── mission/                # agent instruction payload (baked into the image)
│   ├── MISSION.md          #   rules of engagement + objective
│   └── skills/             #   skill catalog the agent reads and executes
├── deploy/                 # Kubernetes manifests (Kustomize)
│   ├── base/               #   namespace, SA, RBAC, ConfigMap, ExternalSecret,
│   │                       #   PVC, Job, killswitch CronJob, CiliumNetworkPolicy
│   ├── overlays/           #   per-environment patches
│   │   ├── local/          #     local k3s (locally-built image)
│   │   └── dev/            #     dev cluster (registry image)
│   └── flags/              #   planted CTF flag Secrets (tier-1 / tier-2)
└── scripts/load-image.sh   # build + import image into k3s on a Multipass VM
```

This repo is a **standalone application**. It was previously a component in an
ArgoCD app-of-apps monorepo; the ApplicationSet/GitOps wiring lived in the
parent repo and has been removed. You deploy it directly with `kustomize`.

---

## How the agent works

`src/redteam_agent/cli.py` is the container ENTRYPOINT. On each run it:

1. Prints identity / RBAC context (`kubectl auth can-i --list`).
2. Runs the **identity canary** — aborts (exit code `2` = INVALID environment)
   if the pod can create `ClusterRoleBindings` or read `kube-system` secrets.
   A CTF pod that powerful means the sandbox is misconfigured.
3. Resolves the LLM backend from env vars and checks it is reachable
   (fails fast, exit `1`, if not).
4. Stages `MISSION.md` + the skill catalog into `/workspace`.
5. Writes the opencode config for the chosen backend and launches opencode
   headless under a wall-clock timeout, teeing output to a per-run log.
6. Guarantees a `findings-*.md` document exists in `/workspace/output`.

Supported backends (via `AGENT_BACKEND`): `openai`, `azure-openai`,
`anthropic`, `ollama`. See [Configuration](#configuration-reference).

---

## Local development (Python + uv)

This project uses [uv](https://docs.astral.sh/uv/). Dependencies are pinned in
`uv.lock`.

```bash
# Install deps into a local .venv (incl. dev tools)
uv sync --extra dev

# Lint
uv run ruff check src/

# Run the wrapper locally (outside Kubernetes).
# With no SA token mounted, kubectl calls fail gracefully and the run is
# recorded as a non-cluster smoke test. A reachable backend is still required.
export AGENT_BACKEND=openai
export OPENAI_API_KEY=sk-...
export AGENT_MAX_ITERATIONS=5
uv run redteam-agent
```

> Running locally still shells out to `opencode` and `kubectl`. For a faithful
> end-to-end test, build the image and run it in the cluster (below).

---

## Build the image

```bash
docker build -t redteam-agent:local-dev .
```

The build installs system recon tooling, `kubectl`, the `opencode` binary, and
creates the uv-managed virtualenv at `/opt/venv`. The `redteam-agent` console
script is the ENTRYPOINT.

### Load into local k3s (Multipass VM)

```bash
./scripts/load-image.sh            # builds, saves, transfers, imports into k3s
# or pass a VM name: ./scripts/load-image.sh my-vm
```

### Push to a registry (dev cluster)

```bash
docker tag redteam-agent:local-dev ghcr.io/<org>/redteam-agent:latest
docker push ghcr.io/<org>/redteam-agent:latest
```

---

## Run it in the cluster

The agent runs as a Kubernetes Job, applied with Kustomize. Inspect the
rendered manifests before applying:

```bash
kubectl kustomize deploy/overlays/local      # render local overlay
kubectl kustomize deploy/overlays/dev         # render dev overlay
```

### 1. Credentials

The Job reads LLM credentials from a Secret named `openai-credentials`
(mounted via `envFrom ... optional: true`). Two ways to provide it:

- **External Secrets Operator (ESO)** — `deploy/base/external-secret.yaml`
  pulls the key from a `ClusterSecretStore` named `secret-store`. Use this if
  your cluster runs ESO.
- **Plain Secret** — if you don't run ESO, create the Secret directly. The key
  name must match the backend (`OPENAI_API_KEY` for `openai`/`azure-openai`,
  `ANTHROPIC_API_KEY` for `anthropic`):

  ```bash
  kubectl create namespace redteam-sandbox
  kubectl -n redteam-sandbox create secret generic openai-credentials \
    --from-literal=OPENAI_API_KEY="sk-..."
  ```

  > If your cluster has no ESO CRDs, remove `external-secret.yaml` from
  > `deploy/base/kustomization.yaml` first (otherwise the apply fails on the
  > unknown `ExternalSecret` kind).

### 2. Plant the CTF flags (optional)

```bash
kubectl apply -k deploy/flags
```

This creates the tier-1 (same-namespace) and tier-2 (cross-namespace) flag
Secrets the agent hunts for.

### 3. Deploy and run

```bash
# Local k3s overlay (uses the locally-loaded image)
kubectl apply -k deploy/overlays/local

# Watch the Job
kubectl -n redteam-sandbox get jobs,pods -w

# Stream the agent's output
kubectl -n redteam-sandbox logs -f job/redteam-agent
```

Findings are written to the `redteam-output` PVC at `/workspace/output`. To pull
them out:

```bash
POD=$(kubectl -n redteam-sandbox get pod -l app=redteam-agent -o name | head -1)
kubectl -n redteam-sandbox cp "${POD#pod/}:/workspace/output" ./output
```

### 4. Re-run

A Job is immutable. To run again, delete and re-apply:

```bash
kubectl -n redteam-sandbox delete job redteam-agent
kubectl apply -k deploy/overlays/local
```

A **killswitch CronJob** (`deploy/base/cronjob.yaml`) hard-deletes stale
`redteam=true` jobs/pods hourly as a safety net.

### Clean up

```bash
kubectl delete -k deploy/overlays/local
kubectl delete -k deploy/flags
```

---

## Configuration reference

Set via the `agent-config` ConfigMap (`deploy/base/configmap.yaml`) or
per-environment overlay patches.

| Environment Variable | Default | Description |
|---|---|---|
| `AGENT_BACKEND` | `openai` | LLM backend: `openai` / `azure-openai` / `anthropic` / `ollama` |
| `AGENT_MAX_ITERATIONS` | `50` | Advisory iteration cap (informs the timeout) |
| `AGENT_TIMEOUT_SECONDS` | `max_iterations × 60` | Wall-clock timeout for the opencode run |
| `OPENAI_API_KEY` | — | OpenAI key (from the `openai-credentials` Secret) |
| `OPENAI_MODEL` | `gpt-4o-mini` | OpenAI model name |
| `OPENAI_API_BASE` | `https://api.openai.com/v1` | OpenAI-compatible base URL |
| `OLLAMA_HOST` | `http://localhost:11434` | Ollama server URL |
| `OLLAMA_MODEL` | `qwen2.5-coder:7b` | Ollama model name |
| `ANTHROPIC_API_KEY` | — | Anthropic key (`anthropic` backend) |
| `ANTHROPIC_MODEL` | `claude-sonnet-4-20250514` | Anthropic model name |
| `AZURE_OPENAI_ENDPOINT` | — | `https://<resource>.openai.azure.com` |
| `AZURE_OPENAI_DEPLOYMENT` | `gpt-4.1` | Azure deployment name (used as the model) |
| `AZURE_OPENAI_API_VERSION` | `2024-02-01` | Azure API version |
| `AZURE_OPENAI_API_KEY` | — | Azure OpenAI key |

### Cost control (hosted backends)

- Use a dedicated project/key with a spend cap.
- Use a cheap model (`gpt-4o-mini`) for iteration; switch to a larger model only
  for documented "real" runs.
- Keep `AGENT_MAX_ITERATIONS` conservative (5–25 for dev).
- Review token usage in the run log after each run.

---

## Security model

- **Namespace-only RBAC.** `researcher-sa` gets a `Role` (not `ClusterRole`)
  scoped to `redteam-sandbox`: read pods/services/configmaps/PVCs and
  get/list secrets. No cluster-scoped grants, ever.
- **Hardened pod.** Non-root (UID 1000), read-only root filesystem, all
  capabilities dropped, `seccompProfile: RuntimeDefault`, namespace enforces
  PSA `restricted`.
- **Default-deny egress.** A `CiliumNetworkPolicy` allows only DNS, the
  in-cluster API server, and the configured LLM API FQDNs; cloud IMDS
  (`169.254.169.254`) is explicitly denied.
- **Identity canary.** The wrapper aborts before doing anything if its identity
  is unexpectedly powerful.
