Warning
Lookout is in active alpha (v0.2.x). APIs, environment variables, and on-disk/wire formats may change between minor releases without notice. Pin to an exact version, review the CHANGELOG before upgrading, and expect breaking changes before v1.0.0.
- 🚀 Quick Start
- 🆕 Recent Updates
- ✨ Features
- 🔐 Authentication
- 🔌 Connection Modes
- 🖥️ Standalone Mode
- ⚙️ Configuration
- 📡 API Reference
- 🔑 Token Security
- ✅ Verify a Release
- 🛡️ Security
- 📋 Audit Logging
- ⭐ Star History
- 🛠️ Built With
- 🤝 Community & Support
Note
v0.2.0 is the current release. Ships Ed25519 per-client authentication, key enrollment, Argon2id token hashing, a read-only MCP server, Prometheus metrics, structured audit logging, and hardened CI/supply-chain infrastructure. See CHANGELOG.md for full release notes.
flowchart LR
subgraph server ["Your server"]
DD["Drydock<br/>(controller + UI)"]
end
subgraph hostA ["Remote host A"]
direction LR
LA["Lookout<br/>(agent)"]
SGA["sockguard<br/>(socket filter)"]
DA["Docker Engine"]
LA -- "filtered socket" --> SGA --> DA
end
subgraph hostB ["Remote host B"]
direction LR
LB["Lookout<br/>(agent)"]
SGB["sockguard<br/>(socket filter)"]
DB["Docker Engine"]
LB -- "filtered socket" --> SGB --> DB
end
DD -- "HTTPS + SSE · X-Dd-Agent-Secret" --> LA
DD -- "HTTPS + SSE · X-Dd-Agent-Secret" --> LB
The Drydock controller connects inbound to each Lookout agent over HTTP/HTTPS (it initiates; Lookout serves). Each agent reaches the Docker Engine only through a sockguard socket filter. Outbound edge mode (the agent dialing the controller, for hosts with no inbound port) is usable end-to-end as of Drydock 1.5 — see Connection Modes.
The strongest posture combines three controls: sockguard (socket-level request filtering so Lookout never touches the raw Docker socket directly), Ed25519 per-request authentication (signed requests, replay protection, no shared secrets), and a hardened container runtime (read_only, cap_drop: ALL, no-new-privileges, secrets-mounted tokens). Run Lookout in standard mode strictly behind a TLS reverse proxy — the Drydock controller connects inbound to it. (Outbound edge mode for hosts with no inbound port is usable end-to-end with Drydock 1.5.)
Step 1 — generate a token and pull the example:
openssl rand -hex 32 > lookout_token.txt
# Download the hardened compose file and its sockguard policy
curl -fsSLO https://raw.githubusercontent.com/CodesWhat/lookout/main/examples/docker-compose.with-sockguard.yml
curl -fsSLO https://raw.githubusercontent.com/CodesWhat/lookout/main/examples/sockguard.yamlStep 2 — start the hardened stack:
docker compose -f docker-compose.with-sockguard.yml up -dThis runs sockguard and Lookout as separate containers sharing a filtered socket volume. Neither container has the raw Docker socket mounted directly; sockguard enforces an allowlist of Docker API operations at the socket level. The full compose file (examples/docker-compose.with-sockguard.yml):
# Lookout + sockguard — two-layer defense.
#
# Sockguard sits between Lookout and the host's Docker socket and writes a
# filtered unix socket into a shared named volume. Lookout talks to that
# filtered socket instead of mounting /var/run/docker.sock directly, so even
# a fully compromised agent is constrained to the explicit API allowlist in
# sockguard.yaml.
#
# Generate a token first:
# openssl rand -hex 32 > lookout_token.txt
services:
sockguard:
image: ghcr.io/codeswhat/sockguard:latest
restart: unless-stopped
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./sockguard.yaml:/etc/sockguard/sockguard.yaml:ro
- sockguard-socket:/var/run/sockguard
environment:
- SOCKGUARD_LISTEN_SOCKET=/var/run/sockguard/sockguard.sock
lookout:
image: ghcr.io/codeswhat/lookout:latest
restart: unless-stopped
depends_on:
- sockguard
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp
ports:
- "3000:3000"
volumes:
- sockguard-socket:/var/run/sockguard:ro
- lookout-stacks:/data/stacks
environment:
- DOCKER_SOCKET=/var/run/sockguard/sockguard.sock
- TOKEN_FILE=/run/secrets/lookout_token
secrets:
- lookout_token
secrets:
lookout_token:
file: ./lookout_token.txt
volumes:
sockguard-socket:
lookout-stacks:Upgrade to Ed25519 key auth (zero shared secrets): generate a keypair with lookout keygen, mount the authorized_keys file, and set AUTHORIZED_KEYS=/etc/lookout/authorized_keys — see Authentication. Use PRIVATE_KEY_FILE for signed edge-mode hellos.
Edge mode variant (outbound WebSocket — early access)
Early access. Edge mode is usable end-to-end: Drydock 1.5 ships the
/api/lookout/wscontroller endpoint (Ed25519-only) and Lookout signs its hello with an Ed25519 key. Drydock 1.5 and Lookout 0.2.2 are both pre-release; full exec robustness under load lands in Lookout 0.2.2.
For hosts behind NAT or a firewall, examples/docker-compose.edge.yml has Lookout dial out to your Drydock controller's edge endpoint (DRYDOCK_URL + /api/lookout/ws); no port is published on the remote host.
services:
lookout:
image: ghcr.io/codeswhat/lookout:latest
restart: unless-stopped
read_only: true
cap_drop:
- ALL
security_opt:
- no-new-privileges:true
tmpfs:
- /tmp
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- lookout-stacks:/data/stacks
environment:
- DRYDOCK_URL=https://drydock.example.com
- TOKEN_FILE=/run/secrets/lookout_token
- AGENT_NAME=edge-host-01
# Key-based hello instead of a shared token:
# lookout keygen → PRIVATE_KEY_FILE=/run/secrets/lookout_key
secrets:
- lookout_token
secrets:
lookout_token:
file: ./lookout_token.txt
volumes:
lookout-stacks:Quick start (evaluation only — not for production)
This is for trying Lookout out locally. Environment-variable tokens are visible in
docker inspectand process listings. Do not use in production — use the hardened deployment above instead.
docker run -d \
--name lookout \
-v /var/run/docker.sock:/var/run/docker.sock \
-p 3000:3000 \
-e TOKEN=$(openssl rand -hex 24) \
ghcr.io/codeswhat/lookout:latestWithout TOKEN (or TOKEN_HASH/AUTHORIZED_KEYS) the API is unauthenticated — anyone who can reach the port controls your Docker daemon.
Binary install (install.sh)
curl -fsSL https://raw.githubusercontent.com/codeswhat/lookout/main/scripts/install.sh | bashLatest release highlights
- v0.2.0 shipped on 2026-06-12 — Ed25519 per-request authentication with signed requests via
X-Lookout-Key-ID/X-Lookout-Timestamp/X-Lookout-Nonce/X-Lookout-Signatureheaders, verified against anauthorized_keysfile. Replay protection via nonce LRU and timestamp window, SIGHUP hot-reload of the key file,lookout keygenCLI subcommand, andX-Lookout-Reasondiagnostic header on 401s. Signed edge-mode hello viaPRIVATE_KEY_FILE. - Key enrollment — optional single-use
ENROLLMENT_TOKEN(POST /api/lookout/enroll) for bootstrapping the first Ed25519 key — burned on first use, rate-limited, and audit-logged. - Argon2id token hashing —
TOKEN_HASH/TOKEN_HASH_FILEwith OWASP-recommended parameters; SHA-256 success cache keeps per-request cost flat. - MCP server — read-only Model Context Protocol endpoint at
/_lookout/mcp(Streamable HTTP, protocol 2025-11-25) for AI assistants (Claude, Cursor, Windsurf). Tools:list_containers,inspect_container,container_logs,host_metrics,container_stats. - Prometheus metrics —
/metricsand/_lookout/metricsexposinglookout_build_info, container count, and host resource metrics. - Structured audit logging —
AUDIT_LOGenv var records auth events, Compose operations, and exec sessions as JSON lines. - Generic REST adapter — headless REST + SSE management API for standalone mode without a Drydock platform connection (
ADAPTER=generic). - Hardened CI & supply chain — SHA-pinned actions, five Go fuzz targets (60s CI / 5m nightly), integration suite against a real Docker daemon, weekly vulnerability scans (govulncheck/grype/gosec), monthly mutation testing, OpenSSF Scorecard, CodeQL, and cosign keyless signing + CycloneDX SBOM + SLSA provenance on every release.
- v0.1.0 shipped on 2025-06-01 — initial release: transparent Docker API proxy, Edge mode WebSocket tunnel, Drydock adapter, SSE event stream, token auth, rate limiting, multi-arch image.
See CHANGELOG.md for the full itemized history.
| Feature | Description | |
|---|---|---|
| 🔄 | Connection Modes | Standard mode (the Drydock controller connects inbound over HTTP/SSE) is the primary integration. Edge mode (agent dials out over WebSocket, for NAT/firewalled hosts) is usable end-to-end as of Drydock 1.5 + Lookout 0.2.2 (both pre-release). |
| 🐳 | Transparent Docker API Proxy | All Docker Engine API paths forwarded to the local daemon — streaming endpoints, exec session hijacking, and long-lived connections included. |
| 🔑 | Ed25519 Per-Client Authentication | Per-request signatures with per-client keys, replay protection via nonce LRU and timestamp window, authorized_keys-style rotation via SIGHUP, zero shared secrets. |
| 🔐 | Argon2id Token Hashing | Hash your token at rest with OWASP-recommended Argon2id parameters; TOKEN_HASH_FILE for Docker secrets support; SHA-256 success cache keeps per-request overhead flat. |
| 🤖 | MCP Server | AI assistants connect to /_lookout/mcp (Streamable HTTP, protocol 2025-11-25). Read-only tools: list_containers, inspect_container, container_logs, host_metrics, container_stats. Env variable values are never transmitted. |
| 📦 | Container Inventory | Full container metadata with dd.* label parsing and SSE broadcasting, including dd:watcher-snapshot events for Drydock compatibility. |
| 📈 | Prometheus Metrics | Host and per-container CPU/memory/network in cAdvisor-compatible format at /_lookout/metrics. Zero external dependencies. |
| 📋 | Audit Logging | Structured JSON of every API call, auth event, exec session, and Compose operation. Disabled by default (single nil check overhead when off). |
| 🖥️ | Host Metrics | CPU, memory, disk, network, and uptime collection. |
| ⚡ | Interactive Exec | Terminal sessions via WebSocket or HTTP hijack with 100 concurrent session cap. |
| 🗂️ | Docker Compose | Full lifecycle management with security hardening — path traversal protection, env var denylist, service name injection prevention. |
| 📡 | SSE Compatibility | Drop-in replacement for existing Drydock agents, including dd:watcher-snapshot full inventory on connect. |
| ✍️ | Signed Supply Chain | Cosign keyless signatures, CycloneDX SBOM, and SLSA provenance on every release. Verifiable without managing signing keys. |
| 🛡️ | Two-Layer Defense | Pair with sockguard so the agent never touches the raw Docker socket directly. |
| 🪶 | Minimal Footprint | Static Go binary, ~10 MB Wolfi (Chainguard) container image. CGO disabled, stripped, no external runtime dependencies. |
| 🔌 | Standalone Mode | ADAPTER=generic provides a clean REST + SSE API on /api/v1/* backed by the local Docker daemon — no Drydock account required. |
Token Authentication (quickstart)
Set TOKEN to a random secret. All requests must supply it via
Authorization: Bearer, X-Lookout-Token, or X-Dd-Agent-Secret.
TOKEN=$(openssl rand -hex 32)
docker run -d --name lookout \
-v /var/run/docker.sock:/var/run/docker.sock \
-e TOKEN="$TOKEN" \
-p 3000:3000 \
ghcr.io/codeswhat/lookout:latestEd25519 Per-Client Key Authentication (recommended)
Ed25519 keypairs give per-client identity with per-request signatures and replay protection. No shared secrets.
Generate a keypair:
# Writes the private key (PEM PKCS#8) and the authorized_keys line to stdout.
lookout keygen -comment "my-platform:prod"Copy the authorized_keys line to the agent host:
# /etc/lookout/authorized_keys (mode 0600)
ed25519 AAAA... my-platform:prod
Start the agent with Ed25519 auth:
docker run -d --name lookout \
-v /var/run/docker.sock:/var/run/docker.sock \
-v /etc/lookout/authorized_keys:/etc/lookout/authorized_keys:ro \
-e AUTHORIZED_KEYS=/etc/lookout/authorized_keys \
-p 3000:3000 \
ghcr.io/codeswhat/lookout:latestKey rotation (zero-downtime):
- Generate a new keypair:
lookout keygen -comment "my-platform:prod:2026-07" - Append the new public key line to the authorized_keys file on the agent host.
- Send
SIGHUPto reload:kill -HUP $(pidof lookout)ordocker kill --signal HUP lookout. Both old and new keys are now active. - Update the platform to use the new private key.
- Remove the old key from the file and send another
SIGHUP.
Token auth (TOKEN/TOKEN_HASH) continues to work alongside Ed25519 — both
can be set simultaneously during migration. The middleware checks for
X-Lookout-Signature first; if absent, it falls back to the token check.
Standard Mode and Edge Mode
Lookout runs an HTTP(S) server; the Drydock controller connects inbound and pulls from it. This is the integration that works today.
- Set when
DRYDOCK_URLis not configured - Drydock authenticates with the
X-Dd-Agent-Secretshared secret (optional mTLS) - Handshake on
GET /api/containers·/api/watchers·/api/triggers, then a long-lived SSE stream onGET /api/events - Transparent Docker API proxy on all paths; agent endpoints under
/_lookout/* - Optional TLS with modern cipher suites (TLS 1.2+)
Lookout initiates an outbound WebSocket to the controller's edge endpoint (DRYDOCK_URL + /api/lookout/ws) for hosts with no inbound port. Both sides are implemented — Drydock 1.5 ships the controller endpoint and Lookout signs an Ed25519 hello — so edge mode is usable end-to-end. Drydock 1.5 and Lookout 0.2.2 are pre-release; full exec robustness under load lands in Lookout 0.2.2. The endpoint is Ed25519-only: set PRIVATE_KEY_FILE and register the public key with Drydock.
- Set when
DRYDOCK_URLis configured along withTOKEN,AUTHORIZED_KEYS, orPRIVATE_KEY_FILE - Targets hosts behind NAT, firewalls, and dynamic IPs
- Auto-reconnect with exponential backoff + jitter; signed hello via
PRIVATE_KEY_FILE
DRYDOCK_URL set + (TOKEN or AUTHORIZED_KEYS or PRIVATE_KEY_FILE) set → Edge Mode (outbound WebSocket)
Otherwise → Standard Mode (inbound HTTP server)
Run without a Drydock platform connection
Run Lookout without any external controller by setting ADAPTER=generic.
You get a clean REST + SSE API on /api/v1/* backed directly by the local
Docker daemon — no Drydock account required.
docker run -d \
--name lookout \
-v /var/run/docker.sock:/var/run/docker.sock \
-e ADAPTER=generic \
-e TOKEN=my-secret \
-p 3000:3000 \
ghcr.io/codeswhat/lookout:latest| Endpoint | Description |
|---|---|
GET /api/v1/version |
Agent version, protocol info |
GET /api/v1/containers |
Cached container inventory |
GET /api/v1/containers/{id}/logs |
Container logs (tail, since, until, follow) |
GET /api/v1/events |
SSE stream of Docker lifecycle events |
TOKEN=my-secret
# Agent version
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/v1/version | jq .
# Container inventory
curl -s -H "Authorization: Bearer $TOKEN" http://localhost:3000/api/v1/containers | jq .
# Last 50 log lines from a container
curl -s -H "Authorization: Bearer $TOKEN" \
"http://localhost:3000/api/v1/containers/my-container/logs?tail=50"
# Stream container logs live
curl -sN -H "Authorization: Bearer $TOKEN" \
"http://localhost:3000/api/v1/containers/my-container/logs?follow=1"
# Stream Docker lifecycle events (SSE)
curl -sN -H "Authorization: Bearer $TOKEN" \
http://localhost:3000/api/v1/eventsEach SSE event is a JSON object:
{
"ts": "2026-06-11T10:00:00Z",
"type": "container",
"action": "start",
"containerId": "abc123def456",
"name": "my-container",
"image": "nginx:latest",
"labels": { "app": "web" }
}A comment heartbeat line (: heartbeat) is written every 30 seconds to keep
the connection alive through proxies.
Environment variable reference
| Variable | Default | Description |
|---|---|---|
DRYDOCK_URL |
-- | WebSocket URL for Edge mode (wss://...) |
TOKEN |
-- | Authentication token (plaintext) |
TOKEN_FILE |
-- | Path to file containing token |
TOKEN_HASH |
-- | Argon2id hash of token (generate with lookout hash-token) |
TOKEN_HASH_FILE |
-- | Path to file containing Argon2id hash |
AUTHORIZED_KEYS |
-- | Path to Ed25519 authorized_keys file (per-client asymmetric auth) |
AUTHORIZED_KEYS_FILE |
-- | Alias for AUTHORIZED_KEYS |
MAX_CLOCK_SKEW_SECONDS |
60 |
Maximum allowed clock skew for Ed25519 request timestamps |
NONCE_LRU_SIZE |
10000 |
In-memory nonce cache capacity for replay protection |
ENROLLMENT_TOKEN |
-- | One-shot bootstrap token for Model C key enrollment |
ENROLLMENT_TOKEN_FILE |
-- | File containing enrollment token |
PRIVATE_KEY_FILE |
-- | Ed25519 private key (PEM PKCS#8) for signing edge-mode hello |
CA_CERT |
-- | Custom CA certificate for Edge mode |
TLS_SKIP_VERIFY |
false |
Skip TLS verification (testing only) |
PORT |
3000 |
HTTP server port |
BIND_ADDRESS |
0.0.0.0 |
Bind address |
TLS_CERT |
-- | Server TLS certificate (Standard mode) |
TLS_KEY |
-- | Server TLS key (Standard mode) |
TRUSTED_PROXIES |
-- | Comma-separated CIDRs of reverse proxies whose X-Forwarded-For is trusted; unset means forwarding headers are ignored |
| Variable | Default | Description |
|---|---|---|
DOCKER_SOCKET |
Auto-detect | Docker socket path |
DOCKER_HOST |
-- | Docker TCP host (alternative) |
STACKS_DIR |
/data/stacks |
Compose stack file directory |
| Variable | Default | Description |
|---|---|---|
AGENT_ID |
UUID v4 | Unique agent identifier |
AGENT_NAME |
Hostname | Human-readable name |
| Variable | Default | Description |
|---|---|---|
HEARTBEAT_INTERVAL |
30 |
Ping interval (seconds) |
WELCOME_TIMEOUT |
30 |
Seconds to await the Drydock welcome message in edge mode |
REQUEST_TIMEOUT |
30 |
Docker API request timeout (seconds) |
RECONNECT_DELAY |
1 |
Initial reconnect delay (seconds) |
MAX_RECONNECT_DELAY |
60 |
Max reconnect delay (seconds) |
LOG_LEVEL |
info |
debug, info, warn, error |
SKIP_DF_COLLECTION |
-- | Disable disk metrics |
AUDIT_LOG |
-- | Audit log sink: stdout, stderr, or a file path; unset disables auditing |
| Variable | Default | Description |
|---|---|---|
ADAPTER |
drydock |
Adapter to use: drydock (Drydock-compatible) or generic (standalone REST/SSE) |
| Variable | Default | Description |
|---|---|---|
DD_AGENT_SECRET |
-- | Backward-compatible auth token |
DD_AGENT_SECRET_FILE |
-- | Backward-compatible token file |
DD_POLL_INTERVAL |
300 |
Container inventory refresh (seconds) |
Health, agent, MCP, Drydock-compatible, and proxy endpoints
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/health |
GET | No | Simple health check — {"status":"ok"} |
/_lookout/health |
GET | No | Health check + Docker connectivity |
/_lookout/health returns HTTP 503 when the Docker daemon is unreachable.
Both endpoints are unauthenticated and safe to use for load-balancer probes
and Docker HEALTHCHECK instructions.
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/_lookout/info |
GET | Yes | Agent version, mode, capabilities |
/_lookout/compose |
POST | Yes | Docker Compose operations |
/_lookout/metrics |
GET | Yes | Prometheus metrics (agent-scoped) |
/metrics |
GET | Yes | Prometheus metrics (compat alias) |
/_lookout/mcp |
POST | Yes | MCP server (JSON-RPC 2.0, protocol 2025-11-25) |
Lookout exposes a read-only Model Context Protocol endpoint
at POST /_lookout/mcp. AI assistants (Claude, Cursor, Windsurf, or any MCP client) can query
live container state through this endpoint using their standard tool-call flow.
Protocol: MCP 2025-11-25 — Streamable HTTP, stateless single-request mode, Content-Type: application/json.
Available tools:
| Tool | Description |
|---|---|
list_containers |
All containers — id, names, image, state, status, labels |
inspect_container(id) |
State, image, env-var count (values never exposed), mounts, networks, restart policy |
container_logs(id, tail) |
Last N lines of stdout/stderr (max 500) |
host_metrics |
CPU, memory, disk, network, uptime snapshot |
container_stats(id) |
One-shot CPU/memory/network stats for a container |
Credential hygiene: inspect_container returns only the count of environment variables —
values are never transmitted, preventing accidental secret leakage.
{
"mcpServers": {
"lookout": {
"command": "curl",
"args": ["-s", "-X", "POST",
"-H", "Content-Type: application/json",
"-H", "Authorization: Bearer YOUR_LOOKOUT_TOKEN",
"http://your-host:3000/_lookout/mcp"],
"type": "http",
"url": "http://your-host:3000/_lookout/mcp",
"headers": {
"Authorization": "Bearer YOUR_LOOKOUT_TOKEN"
}
}
}
}claude mcp add --transport http \
--header "Authorization: Bearer YOUR_LOOKOUT_TOKEN" \
lookout http://your-host:3000/_lookout/mcp{
"mcpServers": {
"lookout": {
"type": "http",
"url": "http://your-host:3000/_lookout/mcp",
"headers": {
"Authorization": "Bearer YOUR_LOOKOUT_TOKEN"
}
}
}
}Replace YOUR_LOOKOUT_TOKEN with the value you set in TOKEN / TOKEN_FILE / TOKEN_HASH.
| Endpoint | Method | Description |
|---|---|---|
/api/events |
GET | SSE event stream (dd:ack, container events) |
/api/containers |
GET | Container inventory |
/api/containers/:id/logs |
GET | Container logs |
/api/containers/:id |
DELETE | Remove container |
/api/watchers |
GET | Watcher components |
/api/triggers |
GET | Trigger components |
All other paths (/*) are transparently proxied to the Docker Engine API, including streaming endpoints and exec session hijacking.
Lookout exposes Prometheus metrics at /_lookout/metrics (and the alias
/metrics). Both require bearer auth.
Prometheus scrape config:
scrape_configs:
- job_name: lookout
scheme: https # or http if TLS not configured
static_configs:
- targets: ["your-host:3000"]
authorization:
type: Bearer
credentials: YOUR_LOOKOUT_TOKEN
tls_config:
# ca_file: /etc/prometheus/lookout-ca.crt # if using custom CA
insecure_skip_verify: falsePlaintext, file-based, and hash-at-rest token options
Warning: Environment variables are visible in
docker inspectand process listings. For production, useTOKEN_FILEorTOKEN_HASH_FILEwith a mounted secret.
# Generate a strong token
TOKEN=$(openssl rand -hex 32)
docker run -e TOKEN="$TOKEN" ... ghcr.io/codeswhat/lookout:latestTOKEN=$(openssl rand -hex 32)
printf '%s' "$TOKEN" > /run/secrets/lookout-token
chmod 600 /run/secrets/lookout-token
docker run -e TOKEN_FILE=/run/secrets/lookout-token \
-v /run/secrets/lookout-token:/run/secrets/lookout-token:ro \
... ghcr.io/codeswhat/lookout:latestStore only an Argon2id hash so the plaintext token never appears in env dumps or config files:
# Generate the hash (token is read from stdin, never argv)
HASH=$(printf '%s' "$TOKEN" | lookout hash-token)
# $argon2id$v=19$m=19456,t=2,p=1$<salt>$<hash>
# Use the hash instead of the plaintext
docker run -e TOKEN_HASH="$HASH" ... ghcr.io/codeswhat/lookout:latestOr write the hash to a file and use TOKEN_HASH_FILE:
printf '%s' "$TOKEN" | lookout hash-token > /run/secrets/lookout-token-hash
docker run -e TOKEN_HASH_FILE=/run/secrets/lookout-token-hash ...Cosign verification for checksums and container images
Lookout releases are signed with Sigstore cosign via GitHub Actions keyless signing. Checksums and container images can be verified without managing signing keys.
TAG=v0.1.0
cosign verify-blob \
--certificate-identity-regexp "https://github.com/CodesWhat/lookout/.github/workflows/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--bundle "checksums.txt.bundle" \
"checksums.txt"TAG=v0.1.0
cosign verify \
--certificate-identity-regexp "https://github.com/CodesWhat/lookout/.github/workflows/.*" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
"ghcr.io/codeswhat/lookout:${TAG}"Each release includes a CycloneDX SBOM attached as a release asset
(lookout-${TAG}-sbom.cdx.json). Download and inspect it with any
CycloneDX-compatible tool, or verify it with cosign the same way as the
checksums file.
Security model summary
- Authentication: Token-based with timing-safe comparison (
crypto/subtle); hash-at-rest viaTOKEN_HASH(Argon2id); Ed25519 per-client keypairs with per-request signatures and replay protection - Rate Limiting: 10 failed auth attempts per IP per minute
- TLS: TLS 1.2+ with modern AEAD cipher suites
- Compose Security: Path traversal protection, env var denylist, service name injection prevention
- Resource Limits: WebSocket (16 MB), response body (100 MB), exec sessions (100 concurrent)
See docs/security-model.md for the full citable spec and CVE mapping.
Structured JSON audit trail for every security-relevant action
Lookout ships structured JSON audit logging for every security-relevant action — a feature that commercial container management platforms lock behind paid tiers.
# Write to a file (opened append-only, mode 0600)
docker run -e AUDIT_LOG=/var/log/lookout-audit.log ...
# Or to stdout/stderr (useful with log aggregators)
docker run -e AUDIT_LOG=stdout ...Auditing is disabled by default (AUDIT_LOG unset). When disabled the overhead is a single nil pointer check per request.
event |
Triggered when |
|---|---|
api_request |
Any authenticated API call completes |
auth_failure |
An invalid token is presented |
rate_limited |
An IP is blocked by the rate limiter |
compose_op |
A Docker Compose operation runs |
exec_start |
An interactive exec tunnel opens |
{"time":"2026-01-15T10:23:45.123456789Z","level":"INFO","msg":"","event":"api_request","actor":"203.0.113.42","method":"POST","path":"/_lookout/compose","outcome":"allowed","status":200,"duration_ms":3.14}Compose operations include additional fields:
{"time":"2026-01-15T10:23:45.200Z","level":"INFO","msg":"","event":"compose_op","actor":"203.0.113.42","operation":"up","stack":"nginx-stack","outcome":"allowed"}Exec tunnel events:
{"time":"2026-01-15T10:24:01.500Z","level":"INFO","msg":"","event":"exec_start","actor":"203.0.113.42","container":"abc123def456","exec_id":"e7f8a9b1","outcome":"allowed"}| Resource | Link |
|---|---|
| Security Model | docs/security-model.md |
| Ed25519 Auth Design | docs/design/ed25519-auth.md |
| Watchtower Migration | docs/migrating-from-watchtower.md |
| Drydock Integration | docs/drydock-integration.md |
| OpenAPI Spec | api/openapi.yaml |
| Changelog | CHANGELOG.md |
| Contributing | CONTRIBUTING.md |
| Code of Conduct | CODE_OF_CONDUCT.md |
| Security Policy | SECURITY.md |
| Releasing | RELEASING.md |
| Examples | examples/ |
| Issues | GitHub Issues |
| Discussions | GitHub Discussions |
Issues, ideas, and pull requests are welcome. Start with CONTRIBUTING.md, use SECURITY.md for private vulnerability disclosure, and use GitHub Discussions for design questions.
Every release image is cosign-signed via GitHub Actions OIDC. Before running a Lookout image in production, verify it with the canonical invocation in the Verify a Release section above.
Built by CodesWhat
