Any break requires three things: knowing the layout, understanding the routine and help from outside or inside
Project description
Breslin
An authorised red-team CTF agent for Kubernetes. It runs 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/createagainst 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:
- Prints identity / RBAC context (
kubectl auth can-i --list). - Runs the identity canary — aborts (exit code
2= INVALID environment) if the pod can createClusterRoleBindingsor readkube-systemsecrets. A CTF pod that powerful means the sandbox is misconfigured. - Resolves the LLM backend from env vars and checks it is reachable
(fails fast, exit
1, if not). - Stages
MISSION.md+ the skill catalog into/workspace. - Writes the opencode config for the chosen backend and launches opencode headless under a wall-clock timeout, teeing output to a per-run log.
- Guarantees a
findings-*.mddocument exists in/workspace/output.
Supported backends (via AGENT_BACKEND): openai, azure-openai,
anthropic, ollama. See Configuration.
Local development (Python + uv)
This project uses uv. Dependencies are pinned in
uv.lock.
# 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
opencodeandkubectl. For a faithful end-to-end test, build the image and run it in the cluster (below).
Build the image
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)
./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)
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:
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.yamlpulls the key from aClusterSecretStorenamedsecret-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_KEYforopenai/azure-openai,ANTHROPIC_API_KEYforanthropic):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.yamlfromdeploy/base/kustomization.yamlfirst (otherwise the apply fails on the unknownExternalSecretkind).
2. Plant the CTF flags (optional)
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
# 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:
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:
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
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_ITERATIONSconservative (5–25 for dev). - Review token usage in the run log after each run.
Security model
- Namespace-only RBAC.
researcher-sagets aRole(notClusterRole) scoped toredteam-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 PSArestricted. - Default-deny egress. A
CiliumNetworkPolicyallows 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.
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file breslin-0.1.0.tar.gz.
File metadata
- Download URL: breslin-0.1.0.tar.gz
- Upload date:
- Size: 13.5 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
13981f6bf4708a3f90e58b04fc380b09a401bf3dc23d31c32f5928db14a236c4
|
|
| MD5 |
7c6ad63dc8cc9334b26f1f5f5a42f180
|
|
| BLAKE2b-256 |
2cf547f74b385636876066d9d8828f3e78c5df308e6830c5447fa75aced524ae
|
File details
Details for the file breslin-0.1.0-py3-none-any.whl.
File metadata
- Download URL: breslin-0.1.0-py3-none-any.whl
- Upload date:
- Size: 17.1 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: uv/0.11.19 {"installer":{"name":"uv","version":"0.11.19","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
890cd45d21d284f858fce1922424ed57cd8c5b468ed3ab1dd8b20c081277f91f
|
|
| MD5 |
96d645c40da805a646f0e23a564ac0f2
|
|
| BLAKE2b-256 |
6dde66eecf74dd431fda678afd4999d18c91fe927c42d007a3c86195c11aa831
|