feat(selfhost): observability stack — logs, alerts, dashboards, reliability hardening - #1239
Conversation
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #1239 +/- ##
=======================================
Coverage 95.03% 95.03%
=======================================
Files 177 177
Lines 19894 19913 +19
Branches 7170 7172 +2
=======================================
+ Hits 18906 18925 +19
Misses 395 395
Partials 593 593
🚀 New features to boost your workflow:
|
| - ./promtail/promtail-config.yml:/etc/promtail/promtail-config.yml:ro | ||
| # Read-only Docker socket — required for docker_sd_configs discovery. Promtail only | ||
| # reads container metadata + log files; the :ro mount blocks any writes to the daemon. | ||
| - /var/run/docker.sock:/var/run/docker.sock:ro |
There was a problem hiding this comment.
P2: Promtail Docker socket mount mischaracterized as read-only safe
The :ro Docker socket mount does not block API writes as claimed.
Correct the comment and consider a Docker socket proxy to restrict API access.
AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.
<file name="docker-compose.yml">
<violation number="1" location="docker-compose.yml:296">
<priority>P2</priority>
<title>Promtail Docker socket mount mischaracterized as read-only safe</title>
<evidence>The comment on line 294 claims the :ro mount "blocks any writes to the daemon", but a UNIX socket bind-mount with :ro only prevents modification of the socket inode. It does NOT restrict Docker API operations. A compromised Promtail container can still use the socket to enumerate containers, inspect configs (including env vars with secrets), read all container logs, and potentially create a privileged container to escape to the host. The same file already acknowledges on line 341 that mounting the Docker socket "grants container-escape risk", contradicting the false reassurance here.</evidence>
<recommendation>Add an accurate security warning: explain that :ro on a socket does not restrict Docker API calls, that the socket grants effective host root access, and consider adding a Docker socket proxy (such as tecnativa/docker-socket-proxy) that exposes only the read-only subset of the API Promtail actually needs.</recommendation>
</violation>
</file>
1c38acc to
56ee0c3
Compare
| restart: unless-stopped | ||
| profiles: ["observability"] | ||
| environment: | ||
| CONTAINERS: "1" # GET /containers/* (list, inspect, logs) |
There was a problem hiding this comment.
P1: Docker socket proxy permits inspect API exposing all container secrets
CONTAINERS: "1" allows GET /containers/{id}/json, which returns full container configs including Config.Env with every secret.
Isolate the proxy on a dedicated network only Promtail can reach, or use Docker secrets to keep credentials out of environment variables.
AI prompt
Check if this security scanner issue is valid. If so, understand the root cause and fix it. If appropriate, update or add tests. Keep the change focused and preserve intended behavior.
<file name="docker-compose.yml">
<violation number="1" location="docker-compose.yml:299">
<priority>P1</priority>
<title>Docker socket proxy permits inspect API exposing all container secrets</title>
<evidence>The docker-proxy service mounts /var/run/docker.sock:ro and is configured with CONTAINERS: "1", which permits all GET /containers/* endpoints. This includes GET /containers/{id}/json, which returns the full container configuration object containing Config.Env — the complete list of environment variables for every container. Since the default Docker Compose network allows all services to communicate, any compromised container in the project can connect to docker-proxy:2375 and exfiltrate secrets such as POSTGRES_PASSWORD, GRAFANA_ADMIN_PASSWORD, database URLs, and API keys from every other container.</evidence>
<recommendation>Place the docker-proxy service on a dedicated internal Docker network that is only shared with Promtail, so other containers cannot reach the proxy. Alternatively, migrate sensitive values from environment variables to Docker secrets or mounted secret files so they do not appear in Config.Env via the inspect endpoint.</recommendation>
</violation>
</file>
ad03441 to
9879009
Compare
…bility hardening Make the self-host Docker stack production-observable out of the box under `docker compose --profile observability up -d`: Logs — add Loki (TSDB schema v13, filesystem store, 14d compactor retention) + Promtail (docker_sd over a read-only socket; parses the app's JSON log lines into `service`/`level` labels with `event` as structured metadata) + a provisioned Grafana Loki datasource. New dashboard rows: live service logs, errors-only, HTTP status/latency, and active alerts. Alerts — 9 Prometheus rules grounded in the real exported metrics (target down, job-failure ratio, dead-letter growth + backlog, queue backlog, Qdrant error rate, Orb export error rate, HTTP 5xx ratio, p95 latency SLO) wired to Alertmanager, which ships silent (null receiver) with commented Slack/email/webhook examples. Metrics — `gittensory_http_requests_total` gains a `status` class label and a new `gittensory_http_request_duration_seconds` histogram; metrics.ts gains histogram support (observe + Prometheus _bucket/_sum/_count rendering), fully covered. Reliability — healthchecks move from /health (a liveness stub) to /ready (DB + migrations applied), with a 60s start_period for the Postgres cold start; a retry around the Qdrant init that otherwise crash-loops on a slow cold start, plus a compose depends_on gate on a real Qdrant /readyz healthcheck; Redis hardened to a bounded cache-only role (256mb, allkeys-lru, RDB+AOF off, volume dropped); Orb export failures now logged. Qdrant ports bound to loopback (the app uses the internal network; the local container is unauthenticated). Docs — document PUBLIC_API_ORIGIN (required by the setup wizards), Qdrant auth/dimension, and the observability log stack in .env.example. Validated live: the full postgres+redis+qdrant+observability stack comes up healthy, logs flow into Loki, an alert fires end-to-end through Alertmanager and resolves on recovery, and promtool/amtool accept the configs.
…vent/error lines The live + errors log panels showed raw JSON blobs. Use Loki's `| json` parsing + `line_format` to render a concise "event — error" line (full fields still available on row expand, level-colored via the promoted `level` label). The errors panel now filters on a non-empty `error` field instead of `level=~"error|warn"`, so it also catches the failing-job logs that are emitted at level=audit (e.g. job_error / job_dead).
…ket proxy Promtail no longer mounts /var/run/docker.sock. A `:ro` socket bind-mount only protects the socket inode — it does NOT restrict the Docker API, so socket access is effectively host root (enumerate containers, read env/secrets + all logs, create a privileged container and escape the host). The previous comment claiming `:ro` "blocks any writes to the daemon" was false. Promtail now reaches the Docker API through a tecnativa/docker-socket-proxy that holds the socket and exposes only the read-only endpoints docker_sd needs (CONTAINERS + NETWORKS, POST denied). Verified live: discovery + log shipping to Loki still work for every container, with no raw socket in the Promtail container.
…work
The observability stack fronts the Docker socket with tecnativa/docker-socket-proxy
so Promtail never touches the raw socket. POST=0 blocks every mutating call, but the
read-only inspect endpoint (GET /containers/{id}/json) still returns each container's
Config.Env — and publishing no host port is not enough, because the default compose
network is reachable by every service. Any compromised container could hit
docker-proxy:2375 and exfiltrate POSTGRES_PASSWORD / GRAFANA_ADMIN_PASSWORD and the
like from its peers.
Put the proxy on a dedicated `internal: true` network shared only with Promtail, so
the inspect endpoint is unreachable from any other service. Promtail keeps the default
network for Loki pushes. Comments updated to explain the real boundary.
9879009 to
b726d1f
Compare
|
Superagent didn't find any vulnerabilities or security issues in this PR. |
Summary
Makes the self-host Docker stack production-observable and resilient out of the box. One command —
docker compose --profile observability up -d— now brings up metrics, logs, alerting, and dashboards, and the core stack is hardened for running real PR reviews.Logs (new): Loki (TSDB schema v13, filesystem store, 14-day compactor retention) + Promtail (discovers the compose project's containers over a read-only Docker socket; parses the app's JSON log lines into
service/levellabels witheventas structured metadata) + a provisioned Grafana Loki datasource. New dashboard rows: live service logs, errors-only, HTTP status/latency, and active alerts.Alerts (new): 9 Prometheus rules grounded only in real exported metrics — target down, job-failure ratio, dead-letter growth + backlog, queue backlog, Qdrant error rate, Orb export error rate, HTTP 5xx ratio, p95 latency SLO — wired to Alertmanager, which ships silent (null receiver) with commented Slack/email/webhook examples so notifications work the moment an operator fills one in.
Metrics:
gittensory_http_requests_totalgains astatusclass label, and a newgittensory_http_request_duration_secondshistogram is added.metrics.tsgains histogram support (observe()+ Prometheus_bucket/_sum/_countrendering).Reliability hardening:
/health(a liveness stub that's 200 even with the DB down) to/ready(DB answers + migrations applied), with a 60sstart_periodfor the Postgres cold start.fetchthat crash-loops on a slow cold start), plus a composedepends_ongate on a real Qdrant/readyzhealthcheck.allkeys-lru, RDB+AOF off, volume dropped — it's ephemeral).Docs:
PUBLIC_API_ORIGIN(required by the setup wizards), Qdrant auth/dimension, and the observability log stack documented in.env.example.Scope
src/server.ts,src/selfhost/metrics.ts) + Docker stack (compose, Dockerfile, prometheus/loki/promtail/alertmanager/grafana configs) +.env.example. No changes to the Cloudflare Worker path.wantedPaths; noblockedPaths; noCHANGELOG.md.--profile observability/--profile qdrant; the default stack is unchanged except the healthcheck endpoint.Validation
Run locally (all green):
npm run test:ci— exit 0npm audit --audit-level=moderate— 0 vulnerabilitiesnpm run typecheck,npx vitest run test/unit/selfhost-metrics.test.ts(histogram tests, 100% branch coverage onmetrics.ts)promtool check config/check rules— valid, 9 rules;amtool check-config— validdocker compose configrenders for the default stack and every profile combinationValidated live in Docker (full
postgres + redis + qdrant + observabilitystack):/ready200 on Postgres (60 migrations); pg-adapter exercised live/metricsshows the new status classes (2xx/4xx) + latency histogram; p95 resolves (0.067s){service="gittensory"},levelfilter); Promtail discovers all containersGittensoryTargetDown(critical) → Prometheus → Alertmanager, and it auto-resolved on recoveryredis-cli config get; rate limiter writes keys under real traffic127.0.0.1Safety
:ro).apps/gittensory-ui) changes — the Grafana dashboard is observability config (JSON), not the product UI, so no UI Evidence table applies.Extends the self-host observability work (#980 / #1206). No behavior change to the hosted Worker.