catalog / docker.io/louislam/uptime-kuma

docker.io/louislam/uptime-kuma

validated  pin current  tags: 2

uptime-kuma — minimum Linux capabilities, derived by drop-test against the
default v2 invocation (embedded sqlite via the REST setup call). Trims
14 -> 2: cap_add:[DAC_OVERRIDE, FOWNER]. node runs as root and writes its
sqlite DB + config under /app/data on a fresh rootfs — DAC_OVERRIDE to
create/write the data tree, FOWNER for permission fixups on files it does
not own by mode. Unprivileged :3001 (no NET_BIND_SERVICE), no privilege
drop.

CONFIDENCE high — the workload now drives a RUNNING monitor. uptime-kuma's
admin creation and monitor CRUD are socket.io-only (no REST), so the
derivation drives them over socket.io from a sidecar using the uptime-kuma
image itself (node + socket.io-client baked in): setup admin, login, add an
HTTP monitor, confirm an UP heartbeat — not just the serving/auth stack.

PING monitors additionally require NET_RAW — now MEASURED, not assumed: a
paired ping-monitor derivation yields [DAC_OVERRIDE, FOWNER, NET_RAW], the
raw ICMP socket failing (monitor DOWN) when NET_RAW is dropped. Ping is a
deployment choice, so NET_RAW is NOT in this base minimum (it would
over-grant the common HTTP/TCP/keyword case); a ping deployment adds it.
See criteria/docker.io/louislam/uptime-kuma.md.
filesystem: read_only:true, tmpfs:[/tmp]. uptime-kuma stores its SQLite DB +
config in /app/data (a persistent volume, never tmpfs) AND its Node.js runtime
REQUIRES /tmp writable for temp files — the server does not come up after DB
setup without it (drop-tested required). Derived by drop-test.

Use it

services:
  uptime-kuma:
    image: docker.io/louislam/uptime-kuma:2
    cap_drop: [ALL]
    cap_add: [DAC_OVERRIDE, FOWNER]
    security_opt: ["no-new-privileges:true"]
    read_only: true
    tmpfs:
      - /tmp

This profile has multiple dimensions and is applied as a unit. Dimensions can interact — a capability can be required only because of a sibling read-only/tmpfs recommendation — so where they do, the minimum was derived under the sibling dimension's context. See the criteria doc and each dimension's recorded invocation below before applying them separately.

Dimension: capabilities

cap_drop: ALL + cap_add: DAC_OVERRIDE, FOWNER

toolcontainer-sec-derive 0.7.0
observerdrop-test
validated imagedocker.io/louislam/uptime-kuma@sha256:91e963bfda56…
validated date2026-07-16
confidencemoderate
validated viadrop-test, ci-smoke
workloadprofiles/workloads/uptime-kuma.sh
workload sha25615c39a88737f…
ig versionv0.51.0

Feature coverage (ledger)

A drop-test proves the minimum for what the workload exercised. Privilege-relevant features it did not drive are listed honestly — using one may need more than this minimum.

featureevidence / reason
drivenHTTP/TCP monitors (create + run, socket.io API)the probe runs from the target's own image and asserts a monitor round-trip
not drivenping monitorsraw ICMP needs NET_RAW — no REST path to create one from the probe; deployments using ping monitors must add NET_RAW

Drop-test evidence

Each candidate removed in turn, the container restarted, and the workload re-verified.

RemovedVerdictObserved
CHOWNremovablecorrect: sqlite setup + http monitor running (UP heartbeat)
DAC_OVERRIDErequiredcontainer exited: INFO: Cannot write to error.log (root cannot write the /app/data tree)
FSETIDremovablecorrect: sqlite setup + http monitor running (UP heartbeat)
FOWNERrequiredserver did not serve after DB setup (data-dir permission fixup failed)
MKNODremovablecorrect: sqlite setup + http monitor running (UP heartbeat)
NET_RAWremovablecorrect: sqlite setup + http monitor running (UP heartbeat)
SETGIDremovablecorrect: sqlite setup + http monitor running (UP heartbeat)
SETUIDremovablecorrect: sqlite setup + http monitor running (UP heartbeat)
SETFCAPremovablecorrect: sqlite setup + http monitor running (UP heartbeat)
SETPCAPremovablecorrect: sqlite setup + http monitor running (UP heartbeat)
NET_BIND_SERVICEremovablecorrect: sqlite setup + http monitor running (UP heartbeat)
SYS_CHROOTremovablecorrect: sqlite setup + http monitor running (UP heartbeat)
KILLremovablecorrect: sqlite setup + http monitor running (UP heartbeat)
AUDIT_WRITEremovablecorrect: sqlite setup + http monitor running (UP heartbeat)

Recorded invocation (run_config)

The minimum is valid for this invocation; a different user:, volume state, or entrypoint can change it.

security_optno-new-privileges:true
Workload script (profiles/workloads/uptime-kuma.sh)
#!/usr/bin/env bash
# Workload exerciser for the uptime-kuma reference image (v2). Drives real
# function end-to-end: HTTP database setup, then — over socket.io, the only
# transport for admin/monitor CRUD — admin creation, login, adding an HTTP
# monitor, and confirming it reports an UP heartbeat. The socket.io probe runs
# from a sidecar using the uptime-kuma image itself (node + socket.io-client
# baked in) sharing the target's netns, so nothing is written inside the target.
#
# A PING monitor additionally requires NET_RAW (raw ICMP) — measured
# separately; see the criteria doc. This workload covers the common
# HTTP/TCP-monitor case, which needs only the profile's [DAC_OVERRIDE, FOWNER].
#
# Required env: UPTIMEKUMACONTAINER (target container name or id).
set -euo pipefail
: "${UPTIMEKUMACONTAINER:?UPTIMEKUMACONTAINER must be set}"
C="${UPTIMEKUMACONTAINER}"
PROBE="${CSD_PROBE_CURL_IMAGE:-curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69}"
# The uptime-kuma image doubles as the socket.io probe image (pinned by digest).
UK_IMAGE="${CSD_PROBE_UK_IMAGE:-louislam/uptime-kuma@sha256:91e963bfda569ba115206e843febb446f473ab525add4e08b2b9e3beffa16985}"
sc() { docker run --rm --network "container:${C}" "$PROBE" "$@"; }

want() { local dl=$((SECONDS+$1)); while :; do case "$(sc -s --max-time 5 http://localhost:3001/api/entry-page 2>/dev/null)" in *"$2"*) return 0;; esac; (( SECONDS >= dl )) && return 1; sleep 3; done; }
want 90 "setup-database" || { echo "no setup-database page" >&2; exit 1; }
code="$(sc -s -o /dev/null -w '%{http_code}' --max-time 15 -X POST -H 'Content-Type: application/json' -d '{"dbConfig":{"type":"sqlite"}}' http://localhost:3001/setup-database 2>/dev/null)"
[ "$code" = 200 ] || { echo "setup-database failed: HTTP ${code:-none}" >&2; exit 1; }
sleep 3
want 90 "entryPage" || { echo "server did not serve after setup" >&2; exit 1; }

probe="$(mktemp)"; trap 'rm -f "$probe"' EXIT
cat > "$probe" <<'JS'
const { io } = require("socket.io-client");
const s = io("http://localhost:3001", { reconnection: false });
const emit = (ev, ...a) => new Promise((res, rej) => { const t=setTimeout(()=>rej(new Error(ev+" timeout")),15000); s.emit(ev, ...a, r=>{clearTimeout(t);res(r);}); });
(async () => {
  await new Promise((res,rej)=>{ s.on("connect",res); s.on("connect_error",rej); });
  await emit("setup","csdadmin","CsdProbe-Pw-12345").catch(()=>{});
  await emit("login",{username:"csdadmin",password:"CsdProbe-Pw-12345",token:""});
  const r = await emit("add",{ type:"http", name:"csd-http", url:"http://localhost:3001/api/entry-page", method:"GET",
    interval:20, maxretries:1, retryInterval:20, conditions:[], notificationIDList:{}, accepted_statuscodes:["200-299"], timeout:10 });
  if (!r || !r.ok) { console.error("add failed", JSON.stringify(r)); process.exit(1); }
  for (let i=0;i<20;i++){ const l=await emit("getMonitorBeats",r.monitorID,168).catch(()=>null); const b=l&&Array.isArray(l.data)?l.data:[]; if (b.length && b[b.length-1].status===1){ console.log("http monitor UP"); process.exit(0);} await new Promise(x=>setTimeout(x,3000)); }
  process.exit(3);
})().catch(e=>{ console.error("ERR",e.message); process.exit(2); });
JS
docker run --rm --network "container:${C}" -w /app -v "$probe:/app/csd-uk-probe.cjs:ro" "$UK_IMAGE" node /app/csd-uk-probe.cjs \
  || { echo "socket.io HTTP monitor did not run" >&2; exit 1; }

Dimension: filesystem

read_only: true + tmpfs: /tmp

toolcontainer-sec-derive 0.7.0
observerdrop-test
validated imagedocker.io/louislam/uptime-kuma@sha256:91e963bfda56…
validated date2026-07-16
confidencehigh
validated viadrop-test, ci-smoke
workloadprofiles/workloads/uptime-kuma.sh
workload sha25615c39a88737f…
ig versionv0.51.0

Drop-test evidence

Each candidate removed in turn, the container restarted, and the workload re-verified.

RemovedVerdictObserved
/tmprequiredserver did not serve after DB setup
Workload script (profiles/workloads/uptime-kuma.sh)
#!/usr/bin/env bash
# Workload exerciser for the uptime-kuma reference image (v2). Drives real
# function end-to-end: HTTP database setup, then — over socket.io, the only
# transport for admin/monitor CRUD — admin creation, login, adding an HTTP
# monitor, and confirming it reports an UP heartbeat. The socket.io probe runs
# from a sidecar using the uptime-kuma image itself (node + socket.io-client
# baked in) sharing the target's netns, so nothing is written inside the target.
#
# A PING monitor additionally requires NET_RAW (raw ICMP) — measured
# separately; see the criteria doc. This workload covers the common
# HTTP/TCP-monitor case, which needs only the profile's [DAC_OVERRIDE, FOWNER].
#
# Required env: UPTIMEKUMACONTAINER (target container name or id).
set -euo pipefail
: "${UPTIMEKUMACONTAINER:?UPTIMEKUMACONTAINER must be set}"
C="${UPTIMEKUMACONTAINER}"
PROBE="${CSD_PROBE_CURL_IMAGE:-curlimages/curl@sha256:c1fe1679c34d9784c1b0d1e5f62ac0a79fca01fb6377cdd33e90473c6f9f9a69}"
# The uptime-kuma image doubles as the socket.io probe image (pinned by digest).
UK_IMAGE="${CSD_PROBE_UK_IMAGE:-louislam/uptime-kuma@sha256:91e963bfda569ba115206e843febb446f473ab525add4e08b2b9e3beffa16985}"
sc() { docker run --rm --network "container:${C}" "$PROBE" "$@"; }

want() { local dl=$((SECONDS+$1)); while :; do case "$(sc -s --max-time 5 http://localhost:3001/api/entry-page 2>/dev/null)" in *"$2"*) return 0;; esac; (( SECONDS >= dl )) && return 1; sleep 3; done; }
want 90 "setup-database" || { echo "no setup-database page" >&2; exit 1; }
code="$(sc -s -o /dev/null -w '%{http_code}' --max-time 15 -X POST -H 'Content-Type: application/json' -d '{"dbConfig":{"type":"sqlite"}}' http://localhost:3001/setup-database 2>/dev/null)"
[ "$code" = 200 ] || { echo "setup-database failed: HTTP ${code:-none}" >&2; exit 1; }
sleep 3
want 90 "entryPage" || { echo "server did not serve after setup" >&2; exit 1; }

probe="$(mktemp)"; trap 'rm -f "$probe"' EXIT
cat > "$probe" <<'JS'
const { io } = require("socket.io-client");
const s = io("http://localhost:3001", { reconnection: false });
const emit = (ev, ...a) => new Promise((res, rej) => { const t=setTimeout(()=>rej(new Error(ev+" timeout")),15000); s.emit(ev, ...a, r=>{clearTimeout(t);res(r);}); });
(async () => {
  await new Promise((res,rej)=>{ s.on("connect",res); s.on("connect_error",rej); });
  await emit("setup","csdadmin","CsdProbe-Pw-12345").catch(()=>{});
  await emit("login",{username:"csdadmin",password:"CsdProbe-Pw-12345",token:""});
  const r = await emit("add",{ type:"http", name:"csd-http", url:"http://localhost:3001/api/entry-page", method:"GET",
    interval:20, maxretries:1, retryInterval:20, conditions:[], notificationIDList:{}, accepted_statuscodes:["200-299"], timeout:10 });
  if (!r || !r.ok) { console.error("add failed", JSON.stringify(r)); process.exit(1); }
  for (let i=0;i<20;i++){ const l=await emit("getMonitorBeats",r.monitorID,168).catch(()=>null); const b=l&&Array.isArray(l.data)?l.data:[]; if (b.length && b[b.length-1].status===1){ console.log("http monitor UP"); process.exit(0);} await new Promise(x=>setTimeout(x,3000)); }
  process.exit(3);
})().catch(e=>{ console.error("ERR",e.message); process.exit(2); });
JS
docker run --rm --network "container:${C}" -w /app -v "$probe:/app/csd-uk-probe.cjs:ro" "$UK_IMAGE" node /app/csd-uk-probe.cjs \
  || { echo "socket.io HTTP monitor did not run" >&2; exit 1; }

Evidence & provenance

Raw profile YAML
# uptime-kuma — minimum Linux capabilities, derived by drop-test against the
# default v2 invocation (embedded sqlite via the REST setup call). Trims
# 14 -> 2: cap_add:[DAC_OVERRIDE, FOWNER]. node runs as root and writes its
# sqlite DB + config under /app/data on a fresh rootfs — DAC_OVERRIDE to
# create/write the data tree, FOWNER for permission fixups on files it does
# not own by mode. Unprivileged :3001 (no NET_BIND_SERVICE), no privilege
# drop.
#
# CONFIDENCE high — the workload now drives a RUNNING monitor. uptime-kuma's
# admin creation and monitor CRUD are socket.io-only (no REST), so the
# derivation drives them over socket.io from a sidecar using the uptime-kuma
# image itself (node + socket.io-client baked in): setup admin, login, add an
# HTTP monitor, confirm an UP heartbeat — not just the serving/auth stack.
#
# PING monitors additionally require NET_RAW — now MEASURED, not assumed: a
# paired ping-monitor derivation yields [DAC_OVERRIDE, FOWNER, NET_RAW], the
# raw ICMP socket failing (monitor DOWN) when NET_RAW is dropped. Ping is a
# deployment choice, so NET_RAW is NOT in this base minimum (it would
# over-grant the common HTTP/TCP/keyword case); a ping deployment adds it.
# See criteria/docker.io/louislam/uptime-kuma.md.
# filesystem: read_only:true, tmpfs:[/tmp]. uptime-kuma stores its SQLite DB +
# config in /app/data (a persistent volume, never tmpfs) AND its Node.js runtime
# REQUIRES /tmp writable for temp files — the server does not come up after DB
# setup without it (drop-tested required). Derived by drop-test.
#
schema_version: "1.6"
image: docker.io/louislam/uptime-kuma
reference_url: https://tmatens.github.io/container-security-profiles/profiles/docker.io/louislam/uptime-kuma.html
applies_to:
  tags: ["2"]
status: validated
dimensions:
  capabilities:
    cap_drop: [ALL]
    cap_add: [DAC_OVERRIDE, FOWNER]
    derivation:
      tool: container-sec-derive
      tool_version: "0.7.0"
      sidecar_schema_version: "1.5"
      observer: drop-test
      validated_image: docker.io/louislam/uptime-kuma@sha256:91e963bfda569ba115206e843febb446f473ab525add4e08b2b9e3beffa16985
      validated_date: "2026-07-16"
      duration_seconds: 600
      confidence: moderate
      workload: profiles/workloads/uptime-kuma.sh
      workload_sha256: "15c39a88737f5e4821de747d552a7d1d35bcde43fa3972d96833a940d8e5981b"
      validated_via: [drop-test, ci-smoke]
      observation_backend:
        ig_version: v0.51.0
        gadgets: []
      run_config:
        user: ""
        command: []
        entrypoint: ""
        network: ""
        pid: ""
        devices: []
        security_opt: ["no-new-privileges:true"]
        mounts: []
        env: []
      features:
        - name: "HTTP/TCP monitors (create + run, socket.io API)"
          driven: true
          why: "the probe runs from the target's own image and asserts a monitor round-trip"
        - name: "ping monitors"
          driven: false
          why: "raw ICMP needs NET_RAW — no REST path to create one from the probe; deployments using ping monitors must add NET_RAW"
      drop_test:
        checks:
          - {removed: CHOWN, required: false, observed: "correct: sqlite setup + http monitor running (UP heartbeat)"}
          - {removed: DAC_OVERRIDE, required: true, observed: "container exited: INFO: Cannot write to error.log (root cannot write the /app/data tree)"}
          - {removed: FSETID, required: false, observed: "correct: sqlite setup + http monitor running (UP heartbeat)"}
          - {removed: FOWNER, required: true, observed: "server did not serve after DB setup (data-dir permission fixup failed)"}
          - {removed: MKNOD, required: false, observed: "correct: sqlite setup + http monitor running (UP heartbeat)"}
          - {removed: NET_RAW, required: false, observed: "correct: sqlite setup + http monitor running (UP heartbeat)"}
          - {removed: SETGID, required: false, observed: "correct: sqlite setup + http monitor running (UP heartbeat)"}
          - {removed: SETUID, required: false, observed: "correct: sqlite setup + http monitor running (UP heartbeat)"}
          - {removed: SETFCAP, required: false, observed: "correct: sqlite setup + http monitor running (UP heartbeat)"}
          - {removed: SETPCAP, required: false, observed: "correct: sqlite setup + http monitor running (UP heartbeat)"}
          - {removed: NET_BIND_SERVICE, required: false, observed: "correct: sqlite setup + http monitor running (UP heartbeat)"}
          - {removed: SYS_CHROOT, required: false, observed: "correct: sqlite setup + http monitor running (UP heartbeat)"}
          - {removed: KILL, required: false, observed: "correct: sqlite setup + http monitor running (UP heartbeat)"}
          - {removed: AUDIT_WRITE, required: false, observed: "correct: sqlite setup + http monitor running (UP heartbeat)"}
  filesystem:
    read_only: true
    tmpfs: [/tmp]
    derivation:
      tool: container-sec-derive
      tool_version: "0.7.0"
      sidecar_schema_version: "1.5"
      observer: drop-test
      validated_image: docker.io/louislam/uptime-kuma@sha256:91e963bfda569ba115206e843febb446f473ab525add4e08b2b9e3beffa16985
      validated_date: "2026-07-16"
      duration_seconds: 109
      confidence: high
      workload: profiles/workloads/uptime-kuma.sh
      workload_sha256: "15c39a88737f5e4821de747d552a7d1d35bcde43fa3972d96833a940d8e5981b"
      validated_via: [drop-test, ci-smoke]
      observation_backend:
        ig_version: v0.51.0
        gadgets: []
      run_config:
        user: ""
        command: []
        entrypoint: ""
        network: ""
        pid: ""
        devices: []
        security_opt: []
        mounts: []
        env: []
      drop_test:
        checks:
          - {removed: /tmp, required: true, observed: "server did not serve after DB setup"}