Skip to content

Give the observability containers least privilege #443

Closed

Description

@HMarzban

Summary

Two observability containers can reach the Docker socket, and the Postgres exporter holds the application's full database URL. A bug in any of them would give much more than monitoring needs: the Docker socket means root on the host, and the database URL means full read and write on application data.

  • Severity: Low (needs a bug in a monitoring container first)
  • Area: docker-compose.observability.yml
  • Source: security review of 2026-10-06, finding L17

Where

  • Alloy mounts the socket: docker-compose.observability.yml:26 (/var/run/docker.sock:/var/run/docker.sock:ro). :ro does not limit the Docker API.
  • cAdvisor mounts all of /var/run: docker-compose.observability.yml:67.
  • postgres-exporter uses the application URL: docker-compose.observability.yml:248 (DATA_SOURCE_NAME: ${DATABASE_URL}).
  • The compose comment near :62-64 calls these mounts read-only and safe, and cites docs/observability.md, which does not exist.

Fix plan

  1. Add a Docker socket proxy (for example tecnativa/docker-socket-proxy, pinned by digest like the other images) that allows only the read endpoints Alloy and cAdvisor use (containers, info). Point both at it, and remove their socket mounts.
  2. Create a pg_monitor login role for the exporter in a migration or the deploy setup, and give the exporter its own POSTGRES_EXPORTER_DSN env var.
  3. Correct the compose comment and remove the dead docs/observability.md reference.

Acceptance criteria

  • No observability container mounts /var/run/docker.sock or /var/run.
  • Container metrics and logs still arrive in Grafana.
  • postgres-exporter connects as a pg_monitor role, not the application role, and its metrics still arrive.

Verify

  • docker compose -f docker-compose.observability.yml config shows no socket mounts and the new DSN.
  • On the server: Grafana dashboards for containers and Postgres still update after deploy.

Activity

  1. added theissue type on Oct 6, 2026
  2. HMarzban commented on Oct 9, 2026

    @HMarzban
    CollaboratorAuthor

    Code is live (73e5bd972): Alloy reads the Docker API through docker-socket-proxy (GET only), and node-exporter no longer sees the host socket. postgres-exporter prefers POSTGRES_EXPORTER_DSN.

    Left before closing (maintainer): create a pg_monitor login role on the DigitalOcean cluster and set POSTGRES_EXPORTER_DSN in .env.observability. Keep DATABASE_URL there until a follow-up moves the deploy check.

  3. HMarzban commented on Oct 19, 2026

    @HMarzban
    CollaboratorAuthor

    Maintainer ruling, 2026-10-19

    The cAdvisor /var/run mount is accepted as a documented exception. Without the containerd socket, container names vanish and the container alerts go NoData.

  4. HMarzban commented on Oct 21, 2026

    @HMarzban
    CollaboratorAuthor

    Status 2026-10-21. The exporter login exists on production: metrics_reader, with POSTGRES_EXPORTER_DSN set in .env.observability.

    One change to the plan: on DigitalOcean managed PostgreSQL, doadmin cannot grant pg_monitor (must have admin option on role "pg_monitor"). It can grant pg_read_all_stats, which covers the stats views postgres-exporter reads. So metrics_reader holds pg_read_all_stats. The self-hosting guide and the backend rules now say this.

    Owed: the next (build): observability deploy, then check that the Postgres panels in Grafana still fill. DATABASE_URL stays in .env.observability until the deploy check moves.

  5. HMarzban commented on Oct 21, 2026

    @HMarzban
    CollaboratorAuthor

    Done on production on 2026-10-21.

    • redis-exporter sends REDIS_PASSWORD, read from .env.observability, and redis_up is 1.
    • postgres-exporter logs in as the read-only metrics_reader (pg_read_all_stats; DigitalOcean refuses pg_monitor). pg_up is 1, and the Grafana Postgres panels have data again.

    Two problems came up, and both are fixed:

    1. DigitalOcean's sample connection string ends in sslmode=verify-full&sslrootcert=/path/to/ca-certificate.crt. That path is a placeholder, so every scrape failed and "Postgres down" fired. The DSN now uses sslmode=require, like DATABASE_URL. The self-host docs now warn about it.
    2. The wal collector calls pg_ls_waldir, which needs pg_monitor. 17a67c262 turns it off with --no-collector.wal; no panel reads pg_wal_*. It goes live with the next observability deploy. Until then, it logs one error per scrape and changes nothing else.

    Follow-up, not required: use sslmode=verify-full with DigitalOcean's real CA file mounted into the exporter.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DevOpsSecuritySecurity, access control, and data exposure

    Type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions