0.28.0 — operational hardening (plan D1–D5): CI deploy gate, versioned images, rate limiting, resource limits, health probes
D1: deploy.sh CI gate — read-only SHA via git ls-remote, Gitea commit-status
poll, portable mkdir deploy lock (macOS, no flock), TOCTOU guard, token
passed via curl --config - (not argv), graceful misconfig tolerance.
D2: version-tagged images — OIKOS_VERSION=v$VERSION, keep-last-3 prune derived
from 'docker compose config --images'; VERSION read after pull.
D3: per-IP rate limiting — new internal/httpapi/ratelimit.go (x/time/rate),
rightmost-XFF, /healthz exempt, ctx-driven sweep; disabled by default.
D4: mem_limit/cpus on all 10 compose services.
D5: staleness-aware health probes — new internal/health package wired into
scheduler (:8093) and notifier (:8094); nomos already had :8092.
Two /review passes hardened the deploy lock, TOCTOU guard, token hygiene,
and XFF handling.
This commit is contained in:
@@ -20,6 +20,8 @@ services:
|
||||
- "5432:5432"
|
||||
volumes:
|
||||
- pg-data:/var/lib/postgresql/data
|
||||
mem_limit: 1g
|
||||
cpus: 2.0
|
||||
healthcheck:
|
||||
test: ["CMD", "pg_isready", "-U", "oikos"]
|
||||
interval: 5s
|
||||
@@ -28,6 +30,7 @@ services:
|
||||
|
||||
# One-shot: run migrations then exit
|
||||
migrate:
|
||||
image: oikos-migrate:${OIKOS_VERSION:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: compose/oikos/Dockerfile
|
||||
@@ -38,9 +41,12 @@ services:
|
||||
OIKOS_DATABASE_URL: postgres://oikos:${OIKOS_DB_PASSWORD:-oikos_dev}@postgres:5432/oikos?sslmode=disable
|
||||
command: ["migrate"]
|
||||
restart: "no"
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
|
||||
# One-shot: ingest seeds then exit
|
||||
seed:
|
||||
image: oikos-seed:${OIKOS_VERSION:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: compose/oikos/Dockerfile
|
||||
@@ -52,9 +58,12 @@ services:
|
||||
OIKOS_SEEDS_DIR: /seeds
|
||||
command: ["seed"]
|
||||
restart: "no"
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
|
||||
# API server (Phase 2)
|
||||
api:
|
||||
image: oikos-api:${OIKOS_VERSION:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: compose/oikos/Dockerfile
|
||||
@@ -76,6 +85,10 @@ services:
|
||||
OIKOS_OIDC_CLIENT_ID: ${OIKOS_OIDC_CLIENT_ID:-otkHBSueHJsYtOHstL6rn5izeGgyOsavp1qA1hod}
|
||||
OIKOS_NOMOS_AGENT_SLUG: ${OIKOS_NOMOS_AGENT_SLUG:-agent:nomos}
|
||||
NOMOS_PROXY_URL: http://nomos:8092
|
||||
# Rate limiting (plan D3). Default off; set OIKOS_API_RATE_LIMIT to a
|
||||
# requests/sec value to throttle runaway agent loops per source IP.
|
||||
OIKOS_API_RATE_LIMIT: ${OIKOS_API_RATE_LIMIT:-}
|
||||
OIKOS_API_RATE_BURST: ${OIKOS_API_RATE_BURST:-}
|
||||
# Infisical secret store (Phase 5)
|
||||
OIKOS_INFISICAL_SITE_URL: ${OIKOS_INFISICAL_SITE_URL:-}
|
||||
OIKOS_INFISICAL_CLIENT_ID: ${OIKOS_INFISICAL_CLIENT_ID:-}
|
||||
@@ -89,6 +102,8 @@ services:
|
||||
command: ["api"]
|
||||
stop_signal: SIGTERM
|
||||
stop_grace_period: 30s
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
# Exists so nomos can wait for the API to actually answer rather than just
|
||||
# for its container to exist — see nomos's depends_on below. wget is
|
||||
# BusyBox's, already in the alpine runtime image, so this adds no
|
||||
@@ -104,6 +119,7 @@ services:
|
||||
|
||||
# Scheduler (Phase 3) — observe loop
|
||||
scheduler:
|
||||
image: oikos-scheduler:${OIKOS_VERSION:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: compose/oikos/Dockerfile
|
||||
@@ -118,6 +134,9 @@ services:
|
||||
OIKOS_SCHEDULER_INTERVAL: "30s"
|
||||
OIKOS_SSH_KEY_PATH: /etc/oikos/ssh_key
|
||||
OIKOS_SSH_USER: root
|
||||
# Liveness probe (plan D5): exposes a staleness-aware /healthz inside
|
||||
# the container; the scheduler bumps it each check pass.
|
||||
OIKOS_HEALTH_LISTEN: ":8093"
|
||||
volumes:
|
||||
- ${OIKOS_SSH_KEY_PATH:-~/.ssh/id_ed25519}:/etc/oikos/ssh_key:ro
|
||||
cap_add:
|
||||
@@ -125,9 +144,18 @@ services:
|
||||
command: ["scheduler"]
|
||||
stop_signal: SIGTERM
|
||||
stop_grace_period: 30s
|
||||
mem_limit: 256m
|
||||
cpus: 1.0
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8093/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 90s
|
||||
|
||||
# Notifier (Phase 3) — Matrix alerts
|
||||
notifier:
|
||||
image: oikos-notifier:${OIKOS_VERSION:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: compose/oikos/Dockerfile
|
||||
@@ -144,6 +172,8 @@ services:
|
||||
OIKOS_MATRIX_USER: ${OIKOS_MATRIX_USER:-@hermes:hubris.network}
|
||||
OIKOS_MATRIX_TOKEN: ${OIKOS_MATRIX_TOKEN}
|
||||
OIKOS_MATRIX_ROOM: ${OIKOS_MATRIX_ROOM:-!alerts:hubris.network}
|
||||
# Liveness probe (plan D5): bumps each approval/reaction tick.
|
||||
OIKOS_HEALTH_LISTEN: ":8094"
|
||||
OIKOS_INFISICAL_SITE_URL: ${OIKOS_INFISICAL_SITE_URL:-}
|
||||
OIKOS_INFISICAL_CLIENT_ID: ${OIKOS_INFISICAL_CLIENT_ID:-}
|
||||
OIKOS_INFISICAL_CLIENT_SECRET: ${OIKOS_INFISICAL_CLIENT_SECRET:-}
|
||||
@@ -152,9 +182,18 @@ services:
|
||||
command: ["notifier"]
|
||||
stop_signal: SIGTERM
|
||||
stop_grace_period: 30s
|
||||
mem_limit: 128m
|
||||
cpus: 0.5
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8094/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 120s
|
||||
|
||||
# Nomos agent gateway (Phase 4) — mesh-published :8092
|
||||
nomos:
|
||||
image: oikos-nomos:${OIKOS_VERSION:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: compose/nomos/Dockerfile
|
||||
@@ -188,12 +227,21 @@ services:
|
||||
- "8092:8092"
|
||||
stop_signal: SIGTERM
|
||||
stop_grace_period: 10s
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
healthcheck:
|
||||
test: ["CMD", "wget", "-q", "-O", "-", "http://127.0.0.1:8092/healthz"]
|
||||
interval: 30s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
|
||||
# Control-room SPA — static build served behind Caddy. The outer
|
||||
# production Caddy (caddy-conf repo, LXC 121) splits /api/*, /mcp,
|
||||
# /agent/* off to api:8090 and sends everything else here; this
|
||||
# container only serves static files with SPA-fallback routing.
|
||||
web:
|
||||
image: oikos-web:${OIKOS_VERSION:-latest}
|
||||
build:
|
||||
context: .
|
||||
dockerfile: compose/web/Dockerfile
|
||||
@@ -202,6 +250,8 @@ services:
|
||||
ports:
|
||||
- "8091:80"
|
||||
stop_signal: SIGTERM
|
||||
mem_limit: 64m
|
||||
cpus: 0.25
|
||||
|
||||
# Redis (required by Infisical — Phase 5)
|
||||
redis:
|
||||
@@ -210,6 +260,8 @@ services:
|
||||
profiles: ["infisical", "full"]
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
mem_limit: 128m
|
||||
cpus: 0.5
|
||||
healthcheck:
|
||||
test: ["CMD", "redis-cli", "ping"]
|
||||
interval: 5s
|
||||
@@ -240,6 +292,8 @@ services:
|
||||
REDIS_URL: redis://redis:6379
|
||||
ports:
|
||||
- "8080:8080"
|
||||
mem_limit: 512m
|
||||
cpus: 1.0
|
||||
|
||||
volumes:
|
||||
pg-data:
|
||||
|
||||
Reference in New Issue
Block a user