Quick Deploy — Control Plane, Bouncer, and First Control
Audience: DevOps / Platform engineers + Control authors Time: under 15 minutes for the UI-first Shadow path · under 60 minutes for full Custom / Helm
This guide is your starting point. Shadow mode is the recommended starting posture. After the Control Plane is up, the Control Plane UI wizard generates the exact Bouncer command — no hand-edited .env.bouncer. Advanced Helm fine-tuning lives under Custom (Kubernetes / Helm).
UI-first path (recommended) — ~15 min
1. Start the Control Plane (one command)
Compose (Kickstart package):
unzip controlcore-kickstart.zip && cd controlcore-kickstart
cp .env.example .env # fill the few REQUIRED secrets once
docker compose up -d
Helm (Custom):
helm upgrade --install controlcore ./controlcore \
-n controlcore --create-namespace \
-f values.quickstart.yaml \
-f values-shadow.yaml
values.quickstart.yaml has eight fields. Full values.yaml is optional — see Custom / Enterprise.
Troubleshooting: If health checks fail, run
docker compose ps/kubectl -n controlcore get podsand confirm PostgreSQL + Redis are Ready. Full reference: Troubleshooting.
2. Open the Control Plane and follow the wizard
- Sign in (first login may require password change).
- You are redirected to Getting Started (
/onboarding/bouncer) until a Bouncer heartbeats. - Wizard steps:
- Choose mode — Shadow (default) or Full Intercept
- Describe resource — name + target host:port
- Copy the generated
docker run/ Helm snippet — paste in a terminal (only step outside the browser) - Live wait — UI polls until the Bouncer registers
- Verify — required health checks (optional components do not block)
- Compliance packs — optional, skippable
The API behind steps 2–3 is POST /api/peps/quick-provision (returns bouncer_id, scoped API key, and the ready-to-paste command). Shadow feature flags are enabled automatically on an empty first boot.
Troubleshooting: If the Live wait step never advances, check Bouncer logs for
PAP_API_URL/API_KEYerrors and confirm the Control Plane URL is reachable from the Bouncer host.
3. Confirm audit
Send traffic through the Bouncer and open Operations → Audit. In Shadow mode you will see observation / would-deny events without blocking.
Classic staged path (~60 min)
The sections below remain for operators who prefer manual .env filling or need every knob documented. Prefer the UI-first path above for first install.
This guide takes you from zero to a working setup — Control Plane running, a Bouncer protecting a resource, and your first control (or shadow observation) with a visible audit decision. Once finished, the Deploy & Operate section covers Custom extended deploy (Kubernetes / Helm), scaling, and reusing your existing SIEM.
Going straight to production / Custom? See Custom (Kubernetes / Helm) and Installation (Helm / DevOps reference). Custom deploy is how you keep Splunk (or another SIEM) as the system of record and leave Grafana/Langfuse off.
Need more than
sandbox+production? Add up to 3 additional environments any time fromSettings → Environments. See Environments for the full add-env runbook and the unified per-env API Key.
Sovereignty defaults (Quick deploy)
- Observability and audit stay inside your host / VPC — no foreign telemetry egress by default.
- Grafana and Langfuse are optional Compose/Helm profiles — leave them disabled for a sub-1-hour path; compliance evidence uses Control Plane audit APIs (and SIEM when you configure it).
- Images and libraries are pulled online from your approved / Canadian-region registries.
By the end of this guide you will have:
- A running Control Plane on your infrastructure
- A Bouncer intercepting traffic to a resource you own
- Your first control authored and deployed to sandbox — either enforcing (Paths A/C) or shadow audit (Path B: observe /
SHADOW_WOULD_DENYwithout blocking) - A visible audit decision confirming the path
Choose your path (~60 min total)
Pick the path that matches your primary outcome. All three share Stages 1 and 4; Stage 2 and Stage 3 differ by topology and first control template. Path B (shadow mode) is the recommended first step for every new deployment — observe would-deny telemetry before switching to enforce.
| Path | Best for | Stage 2 focus | Stage 3 focus |
|---|---|---|---|
| Path B — Compliance discovery (shadow) | Recommended first — map gaps before blocking | Shadow Bouncer with BOUNCER_ENFORCEMENT_MODE=shadow | Compliance pack observation controls (no blocking; audit shows would-deny) |
| Path A — Legacy Modernization | On-prem APIs, monoliths, DNS/VIP swap | BOUNCER_TYPE=reverse-proxy in front of the legacy host | Access-control template for authenticated API traffic |
| Path C — AI Governance | LLM, agents, MCP workloads | Egress Bouncer between app and model provider | AI governance template (prompt guardrails, token budget) |
Which deployment mode?
| Your situation | Recommended mode | Guide |
|---|---|---|
| New to Control Core | Shadow — observe and fingerprint without blocking | Deployment modes — Shadow |
| Kubernetes fleet | Sidecar — co-locate PEP with critical pods | Deployment modes — Sidecar |
| Legacy on-prem / VM | Reverse proxy — DNS or VIP swap to Bouncer :8080 | Deployment modes — Reverse proxy |
After the Bouncer container starts, it auto-registers with the Control Plane within ~30 seconds. Verify in Settings → PEPs (status Active) or:
curl -s -H "Authorization: Bearer <token>" "http://localhost:8082/health/bouncers" | jq .
Troubleshooting: If the Bouncer does not appear after 60 seconds, check
docker compose logs bouncerforPAP_API_URLorAPI_KEYerrors. Full reference: Deployment modes, Troubleshooting.
Before you start
| Requirement | Minimum |
|---|---|
| Linux VM (or local Docker) | 2 vCPU, 4 GB RAM |
| Docker Engine | 20.10+ |
| Docker Compose | 2.0+ |
| Ports available | 3000, 8080, 8082, 7000 |
| GitHub repo (for controls) | Any private repo + Personal Access Token |
| Deployment package | Download from info@controlcore.io or your account |
Troubleshooting: Unsure which ports are in use? Run
ss -tlnp | grep -E '3000|8080|8082|7000'before starting. If any port is taken, map the service to a free port in your.envfile using the_PORToverride vars.
Stage 1 — Deploy the Control Plane (~20 min)
TL;DR — 6 commands to a running Control Plane
# 1. Extract your package
unzip controlcore-kickstart.zip && cd controlcore-kickstart
# 2. Copy and fill the env template
cp .env.example .env
# Edit .env: set DATABASE_*, REDIS_*, SECRET_KEY, JWT_SECRET_KEY,
# CC_BUILTIN_ADMIN_PASS, GITHUB_TOKEN, POLICY_REPO_URL,
# and CONTROL_PLANE_PUBLIC_URL
# 3. Start the stack
docker compose up -d
# 4. Wait for health (run until all return "healthy")
watch docker compose ps
# 5. Verify the Control Plane API
curl -s http://localhost:8082/api/v1/health | jq .status
# 6. Open the console
open http://localhost:3000
Expected output of step 5: "ready" — if you see this, Stage 1 is done.
Detailed steps
Step 1 — Fill .env values (~5 min)
The minimum required variables are:
# Control Plane identity
CONTROL_PLANE_PUBLIC_URL=https://controlplane.yourcompany.com # or http://localhost:3000 for local
# Database (PostgreSQL)
DATABASE_URL=postgresql://ccuser:ccpassword@db:5432/controlcore
# Cache (Redis)
REDIS_URL=redis://:redispassword@redis:6379/0
# Secrets (generate with: openssl rand -hex 32)
SECRET_KEY=<32-char-random>
JWT_SECRET_KEY=<32-char-random>
# Admin password (Kickstart default; change after first login)
CC_BUILTIN_ADMIN_PASS=<set-via-secret-manager>
# GitHub controls repo
GITHUB_TOKEN=<your-github-pat>
POLICY_REPO_URL=https://github.com/your-org/your-controls-repo
POLICY_REPO_BRANCH=main
Troubleshooting: If
docker compose up -dfails withport already allocated, the port is in use. AddCONTROL_PLANE_UI_PORT=3001(or similar) to.envto use a different port. See Deployment Guide for full port mapping vars.
Step 2 — Start the stack (~5 min)
docker compose up -d
docker compose ps # all services should be "healthy" within 60–90 seconds
Troubleshooting: If a service stays
startingafter 2 min:
docker compose logs control-plane-api— look forDATABASE_URLorREDIS_URLerrorsdocker compose logs db— check PostgreSQL started correctly- Verify env values:
docker compose config | grep DATABASE_URLFull reference: Troubleshooting — Deployment issues
Step 3 — First login (~2 min)
- Open
http://localhost:3000(or yourCONTROL_PLANE_PUBLIC_URL) - Log in with username
adminand the password you set inCC_BUILTIN_ADMIN_PASS - Go to Profile → Change Password — do this now
Step 4 — Create API keys (API-first preferred) (~3 min)
- API-first path: open
http://localhost:8082/devdocs - Call
POST /developer-portal/token - Call
POST /developer-portal/api-keys/{environment}/generateforsandboxandproduction - Optional UI path: Settings → Environments
Troubleshooting: If Settings → Environments shows no environments, check that the Control Plane API container (
control-plane-api) is healthy. Runcurl http://localhost:8082/api/v1/health— if it returns an error, checkdocker compose logs control-plane-api.
**Stage 1 done ** — You have a running Control Plane. The console is accessible and you have API keys.
Stage 2 — Deploy a Bouncer to protect a resource (~15 min)
A Bouncer is the enforcement component you place in front of the resource you want to protect. Requests go through the Bouncer, which enforces your controls, before reaching the resource.
You need: the Sandbox API key from Stage 1, and a running resource (API, app, or service) to protect. The Bouncer will act as a reverse proxy in front of it.
Bouncer configuration
Create a new .env.bouncer file (or a separate Compose file):
# Bouncer identity
BOUNCER_ID=bouncer-sandbox-01
BOUNCER_NAME=My First Bouncer
BOUNCER_TYPE=reverse-proxy # or: sidecar
ENVIRONMENT=sandbox
# Connect to Control Plane
PAP_API_URL=http://localhost:8082 # use your CONTROL_PLANE_PUBLIC_URL
API_KEY=<your-sandbox-api-key> # from Stage 1 Step 4
# Resource being protected
RESOURCE_NAME=My API # human-readable label (used to pair Sandbox+Production)
RESOURCE_TYPE=api # api | webapp | database | ai-agent | mcp-server
TARGET_HOST=my-api:8080 # internal hostname:port of your resource
ORIGINAL_HOST_URL=https://api.yourcompany.com # external URL your clients use
# Security posture (fail-closed by default)
SECURITY_POSTURE=deny-all
Start the Bouncer
docker compose -f docker-compose.bouncer.yml --env-file .env.bouncer up -d
# Verify the Bouncer is healthy
curl -s http://localhost:8080/health | jq .status
Expected: "ready"
Confirm registration in the Control Plane (or API)
- Open the console → Settings → PEPs
- Your Bouncer (
My First Bouncer) should appear with status Active - Go to Settings → Resources —
My APIshould auto-appear
API-first check:
curl -s -H "Authorization: Bearer <token>" "http://localhost:8082/health/bouncers" | jq .
curl -s -H "Authorization: Bearer <token>" "http://localhost:8082/resources" | jq .
Troubleshooting: Bouncer not appearing after 30 seconds?
docker compose logs bouncer— look forPAP_API_URLconnection errors- Verify
PAP_API_URLis reachable from the Bouncer container:docker exec bouncer curl -s $PAP_API_URL/api/v1/health- Confirm
API_KEYmatches the Sandbox key in Settings → Environments Full reference: Bouncer Deployment, Troubleshooting
**Stage 2 done ** — A Bouncer is running and your resource is registered in the Control Plane.
Stage 3 — Author your first control (~20 min)
In Control Core, controls are the authorization rules that decide whether a request is allowed or denied. Controls are implemented as Rego policies — but you don't need to write Rego to get started. The Visual Builder generates it for you.
Option A — Visual Builder (fastest if you want UI)
Step 1 — Create a new control (~5 min)
- Go to Controls → Create Control
- Choose Visual Builder
- On page 1: select the Resource (
My API) and the Bouncer (My First Bouncer) - Name:
allow-authenticated-users - Environment: Sandbox
Step 2 — Define a simple condition (~5 min)
In the Visual Builder:
- Click Add Condition
- Subject attribute:
user.authenticated - Operator:
equals - Value:
true - Effect: Allow
This control says: "allow requests where the user is authenticated."
What is
user.authenticated? This attribute comes from the request JWT or session. If you connect an identity provider (Okta, Auth0, etc.) as a PIP later, you can use richer attributes likeuser.departmentoruser.role. For now,user.authenticatedis available from the request context without a PIP.
Step 3 — Generate Rego and save (~2 min)
- Click Preview Rego — review the generated code
- Click Save Draft
Step 4 — Deploy to Sandbox (~3 min)
- Click Deploy to Sandbox
- Confirm the deployment status shows Active
Troubleshooting: Deploy fails with "Policy Bridge not connected"?
- Go to Settings → Controls Repository and confirm the GitHub repo is connected
- Click Test connection — if it fails, check your
GITHUB_TOKENhasreposcope- Full reference: Admin Troubleshooting
Option B — Start from a template (~5 min)
- Go to Controls → Templates
- Browse the Scenario Starter Catalog (AI Security, Data Governance, API Security, etc.)
- Click any template → Use this template
- Customise the pre-filled conditions for your resource
- Deploy to Sandbox
Option C — API-first / OEM control deployment (~5 min)
Use this when parent workflows are agent-driven and tenants should not manage controls in UI.
# 1) Validate policy-as-code
curl -s -X POST "http://localhost:8082/policies-as-code/validate" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"policy_name":"allow-authenticated-users","policy_content":"package access\nimport rego.v1\ndefault allow = false\nallow if { input.user.authenticated == true }"}'
# 2) Deploy to sandbox
curl -s -X POST "http://localhost:8082/policies-as-code/deploy" \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"policy_name":"allow-authenticated-users","environment":"sandbox","resource_id":"1","policy_content":"package access\nimport rego.v1\ndefault allow = false\nallow if { input.user.authenticated == true }"}'
Verify the control is enforcing
Send a test request through the Bouncer:
# Without authentication — should be DENIED
curl -i http://localhost:8080/any-endpoint
# With a valid bearer token — should be ALLOWED
curl -i http://localhost:8080/any-endpoint \
-H "Authorization: Bearer <your-jwt>"
Stage 4 — Confirm the audit decision (~5 min)
- Go to Controls → Audit Logs (or Dashboard)
- You should see recent
ACCESS_GRANTEDandACCESS_DENIEDevents for your resource - Click any event to inspect the full decision: subject, resource, action, outcome, and matched control
Troubleshooting: No audit events visible?
- Confirm traffic is going through the Bouncer (port 8080), not directly to your resource
- Check
docker compose logs bouncer | grep -i "decision"- Confirm the Bouncer's
ENVIRONMENT=sandboxmatches the environment shown in the console
**Stage 4 done ** — You have a real enforcement decision in audit logs.
You're done
Your Control Core stack is live:
| Component | Status |
|---|---|
| Control Plane | Running on your infrastructure |
| Bouncer | Protecting My API in Sandbox |
| First control | Active and enforcing |
| Audit decisions | Visible in the console |
Next steps
Author Controls
Learn the full control authoring lifecycle: Visual Builder, Rego editor, templates, and SCCA.
View guideDataConnect a Data Source (PIP)
Connect Okta, Workday, or any identity/HR system to enrich controls with real user and resource context.
View guideOperationsPromote to Production
Add a Production Bouncer and promote your sandbox control to enforce on live traffic.
View guideSupportTroubleshooting
Quick diagnostic commands, common error patterns, and fix guides.
View guideCommon first-run issues
| Symptom | Likely cause | Fix |
|---|---|---|
control-plane-api container exits immediately | Missing required env var | docker compose logs control-plane-api — look for KeyError or missing config |
| Console opens but login fails | Wrong admin password | Check CC_BUILTIN_ADMIN_PASS in .env; restart: docker compose restart control-plane-api |
| Bouncer shows "Disconnected" in console | Wrong PAP_API_URL or API_KEY | Verify URL is reachable from Bouncer container; confirm API key matches Sandbox key |
| Control deploy fails ("Policy Bridge not connected") | GitHub repo not connected | Settings → Controls Repository → Test connection → fix token scope |
| No audit events after sending requests | Traffic bypassing Bouncer | Ensure requests go to port 8080 (Bouncer), not directly to the resource |
curl returns 403 for all requests | SECURITY_POSTURE=deny-all with no active control | Deploy and activate a control in Sandbox first |