Containerized Cobalt Strike 4.12 team server with automatic REST API startup.
git clone https://github.com/Maleick/Cobalt-Docker.git && cd Cobalt-Docker
./cobalt-docker.shThe setup wizard will prompt you for:
- Cobalt Strike license key (required)
- Team server password (required)
- Tailscale auth key (optional — press Enter to skip)
- Container hostname (if Tailscale is enabled)
If .env already exists with valid values, the wizard is skipped and deployment starts immediately.
To deploy with a custom Malleable C2 profile:
./cobalt-docker.sh custom.profile- Builds and runs a Cobalt Strike 4.12 team server inside a Docker container with a single command.
- Starts the REST API automatically —
csrestapilaunches alongside teamserver and displays a bearer token on startup. - Validates everything before launch — preflight checks catch misconfiguration before Docker build/run.
- Works on macOS and Linux — automatic host detection, Docker Desktop mount fallback, and OrbStack QEMU support.
| Key | Default | Description |
|---|---|---|
COBALTSTRIKE_LICENSE |
— | Your Cobalt Strike license key |
TEAMSERVER_PASSWORD |
— | Password for team server authentication |
| Key | Default | Description |
|---|---|---|
REST_API_USER |
csrestapi |
REST API authentication username |
REST_API_PUBLISH_PORT |
50443 |
Host port for REST API |
REST_API_PUBLISH_BIND |
127.0.0.1 |
Host bind address for REST API |
SERVICE_BIND_HOST |
0.0.0.0 |
In-container bind address for csrestapi |
SERVICE_PORT |
50443 |
In-container port for csrestapi |
UPSTREAM_HOST |
127.0.0.1 |
Teamserver host that csrestapi connects to |
UPSTREAM_PORT |
50050 |
Teamserver port that csrestapi connects to |
HEALTHCHECK_URL |
https://127.0.0.1:${SERVICE_PORT}/health |
REST API health endpoint |
HEALTHCHECK_INSECURE |
true |
Allow self-signed TLS for health checks |
| Key | Default | Description |
|---|---|---|
COBALT_LISTENER_BIND_HOST |
0.0.0.0 |
Host bind address for C2 listener ports (80/443/53). Set to 127.0.0.1 if other services hold those ports |
TEAMSERVER_HOST_OVERRIDE |
— | Override auto-detected host IP passed to teamserver |
| Key | Default | Description |
|---|---|---|
TS_AUTHKEY |
— | Tailscale auth key for joining a Tailnet (ephemeral recommended) |
TS_API_KEY |
— | Tailscale API key for automation |
TS_EXTRA_ARGS |
— | Extra arguments for tailscale up (e.g., --hostname=cobalt-docker) |
TS_USERSPACE |
false |
Use userspace networking (required on macOS / environments without TUN) |
USE_TAILSCALE_IP |
false |
Override teamserver host with the Tailscale IPv4 address |
These are shell variables passed when invoking cobalt-docker.sh, not stored in .env:
| Variable | Default | Description |
|---|---|---|
DOCKER_PLATFORM |
linux/amd64 |
Docker --platform flag |
MOUNT_SOURCE |
repo directory | Bind-mount source override (use when Docker cannot see the repo path) |
COBALT_DOCKER_MOUNT_SOURCE |
— | Legacy alias for MOUNT_SOURCE |
Place your .profile files in the profiles/ directory. An example profile is included to get started.
Profiles are automatically linted with c2lint before deployment. If linting fails, the server does not start.
# Deploy with the included reference profile
./cobalt-docker.sh profiles/reference.profile
# Deploy with your own profile
cp /path/to/your/custom.profile profiles/
./cobalt-docker.sh profiles/custom.profile
# Lint a profile without deploying
./cobalt-docker.sh lint profiles/reference.profileThe REST API starts automatically. No extra flags needed — the entrypoint launches csrestapi after confirming teamserver TLS readiness, then auto-logs in and displays a bearer token.
Verify it is working:
PORT="${REST_API_PUBLISH_PORT:-50443}"
# HTTP readiness (auth endpoints may return 401/403)
curl -ksS -o /dev/null -w '%{http_code}\n' "https://127.0.0.1:${PORT}/health"
# TLS negotiation
openssl s_client -connect "127.0.0.1:${PORT}" -servername localhost -brief </dev/nullMCP integration: You can build MCP tooling on top of the REST API (see the Cobalt Strike blog for an example using FastMCP). The OpenAPI spec is available at https://127.0.0.1:50443/v3/api-docs.
Tailscale provides secure access to the team server over a private Tailnet without exposing ports publicly.
TS_AUTHKEY="tskey-auth-..."
USE_TAILSCALE_IP="true"
TS_USERSPACE="true" # required on macOS / no TUN
TS_EXTRA_ARGS="--hostname=cobalt-docker"- Stable IP —
USE_TAILSCALE_IP=truebinds teamserver to the Tailscale IPv4 address. - Ephemeral nodes — use ephemeral auth keys so containers auto-remove from the Tailnet on stop.
- Local-only mode — leave
TS_AUTHKEYempty to skip Tailscale entirely.
The entrypoint logs deterministic phase markers for triage:
| Marker | What happens |
|---|---|
STARTUP[preflight] |
Validates inputs, binaries, port ranges, boolean flags |
STARTUP[tailscale] |
Starts tailscaled, authenticates (only when TS_AUTHKEY is set) |
STARTUP[teamserver-launch] |
Starts teamserver with --experimental-db |
STARTUP[teamserver-ready] |
TLS readiness confirmed via openssl s_client probe (60s timeout) |
STARTUP[rest-launch] |
Starts csrestapi |
STARTUP[rest-ready] |
HTTPS health check passes (HTTP 2xx-4xx = reachable) |
STARTUP[rest-token] |
Auto-login and bearer token display |
STARTUP[monitor] |
Both processes supervised; container exits if either dies |
| Port | Protocol | Purpose |
|---|---|---|
50050 |
TCP | Teamserver |
80 |
TCP | HTTP listener (bound to COBALT_LISTENER_BIND_HOST) |
443 |
TCP | HTTPS listener (bound to COBALT_LISTENER_BIND_HOST) |
53 |
UDP | DNS listener (bound to COBALT_LISTENER_BIND_HOST) |
${REST_API_PUBLISH_PORT} |
TCP | REST API (bound to REST_API_PUBLISH_BIND, localhost-only by default) |
- Docker — Docker Desktop, OrbStack, or any Docker-compatible runtime
- A valid Cobalt Strike license key
That's it. Tailscale is installed inside the container — no host installation needed.
The container runs detached in the background. The script waits for startup to complete, displays the bearer token and connection info, then returns you to the command line. You can close the terminal — the container keeps running.
./cobalt-docker.sh status # check if running
./cobalt-docker.sh api-token # get a REST API bearer token
./cobalt-docker.sh stop # stop and remove the container
./cobalt-docker.sh help # show all commands
docker logs -f cobaltstrike_server # follow logsAfter a machine reboot, the container restarts automatically. Run ./cobalt-docker.sh api-token to get a new bearer token (tokens reset on restart).
Stop the container:
./cobalt-docker.sh stopFull cleanup — remove the Docker image and .env:
./cobalt-docker.sh stop
docker rmi cobaltstrike:latest
rm -f .envQuick diagnostics:
PORT="${REST_API_PUBLISH_PORT:-50443}"
docker logs cobaltstrike_server # 1. Check startup phases
curl -ksS -o /dev/null -w '%{http_code}\n' "https://127.0.0.1:${PORT}/health" # 2. HTTP readiness
openssl s_client -connect "127.0.0.1:${PORT}" -servername localhost -brief </dev/null # 3. TLS check
docker inspect cobaltstrike_server # 4. Verify env/port wiringPreflight failures — cobalt-docker.sh exits before build/run when:
.envis missing or required keys are empty- Port values are not integers in 1-65535
- Boolean settings are not
trueorfalse TEAMSERVER_HOST_OVERRIDEcontains whitespace- Host target auto-detection fails without an override set
This project is developed and tested on macOS (Apple Silicon) using OrbStack as the Docker runtime.
- Rosetta mode (OrbStack default) — teamserver runs fine, but
csrestapimay fail with an AVX2 CPU feature error. Disable "Use Rosetta to run Intel code" in OrbStack → System → Compatibility if this happens. - QEMU mode (Rosetta disabled) — full AVX2 support. Both teamserver and csrestapi run correctly.
- Native x86 hardware — no emulation settings needed. Everything works out of the box.
The project also works with Docker Desktop on macOS and Linux. Not tested on Windows.
- Cobalt Strike by Fortra — a valid license is required.
- Docker community for container tooling and documentation.
- White Knight Labs docker-cobaltstrike
- warhorse/docker-cobaltstrike
- ZSECURE/zDocker-cobaltstrike
- Blog post by Ezra Buckingham
This project is for authorized and ethical use only. The author is not responsible for misuse.