Skip to content

Containers

OpenSCM treats application containers (Docker, Podman) as first-class inventory under each Linux host. Containers appear nested under their host in the Systems list, get a full configuration detail panel, and can be tested with two new container-only elements (IMAGE, NETWORK). Discovery is automatic — no per-container enrollment, no in-container agent.

OS containers vs app containers

"Container" in OpenSCM means an app container — Docker, Podman, eventually Kubernetes pods. OS containers (LXC, LXD, BSD jails) are full Linux userspaces with their own init systems; install the OpenSCM agent inside them, treat them as regular systems, and you get the full host test surface inside. The host running them does not appear to have container inventory in OpenSCM, and that's correct.


Discovery

Each Linux agent enumerates containers on every heartbeat. For each runtime detected (docker --version or podman --version exits 0), it lists the running containers via <runtime> ps --format json and pulls the configuration metadata via <runtime> inspect <id>. The list ships with the heartbeat payload.

Runtime Detection Listing
Docker docker CLI on $PATH docker ps --format '{{json .}}'
Podman podman CLI on $PATH podman ps --format json
Kubernetes (not yet — planned for a future release)

Running containers only (since 0.6.1)

Inventory tracks running (and paused) containers — not stopped/exited ones. Stop a container and it drops off the host's next heartbeat report, and the server removes it from the inventory. A host with a working runtime but zero running containers reports an explicit empty set, so its last container is cleared too. (If the runtime's daemon is unreachable, the agent reports nothing rather than an empty set, so a transient outage never wipes a host's inventory.)

The agent soft-fails on missing runtime or permission errors — if the agent isn't in the docker group, Docker discovery is skipped silently and the heartbeat continues. Non-Linux platforms (macOS, Windows, FreeBSD) return an empty list without any shell-outs.

Permissions

The agent typically runs as root on a host, so Docker socket / Podman CLI access is automatic. For non-root deployments:

  • Docker — the agent user must be in the docker group, OR the agent process must have read access to /var/run/docker.sock.
  • Rootless Podman — the agent only sees containers owned by the user it's running as. This is usually correct; document if it matters for your environment.

Inventory in the Systems list

Systems with at least one container get a left-side chevron in the Managed Systems table. Click to expand a nested table showing every container on that host:

▶ ID  Name              OS              IP            Agent  Last Seen
▼ 12  web-host-01       Ubuntu 24.04    192.168.1.50  v0.6.2  30s ago
   ├─ 🐳 nginx-prod      nginx:1.27-alpine   running  172.17.0.2
   ├─ 🐳 redis           redis:7-alpine      running  172.17.0.3
   └─ 🦭 worker          internal/job:42     running  172.17.0.4

Runtime icons: 🐳 Docker, 🦭 Podman. Clicking a container row opens the Container Details modal with the full configuration snapshot:

Field Source
Image + tag + digest inspect.Image
IP NetworkSettings.IPAddress (or first network in Networks)
Run user Config.User
Network mode HostConfig.NetworkMode
Privileged HostConfig.Privileged
Read-only filesystem HostConfig.ReadonlyRootfs
Restart policy HostConfig.RestartPolicy.Name
Health check defined Config.Healthcheck present
Exposed ports NetworkSettings.Ports
Mounts Mounts (source, destination, type, ro)
Added capabilities HostConfig.CapAdd
First / last seen OpenSCM timestamps (preserved across rebuilds)

first_seen is preserved across container rebuilds — useful for "how long has this container been running on this host" questions. Containers that disappear from a heartbeat are deleted immediately if the agent reports an empty list, or aged out after Container Retention (days) if the host itself stops reporting.


Retention

Configurable per-tenant under Admin → Settings → General → Container Retention (days). Default is 7; set to 0 to keep forever. Container churn is normally high so the default is intentionally short — long-lived containers stay because each heartbeat refreshes last_seen. Successful trims write a retention.containers_pruned audit row with the count.

A second cleanup happens immediately on every heartbeat: containers the agent stopped reporting since the previous tick are deleted right away (stragglers from the previous scan). This keeps the UI matching reality without waiting for the daily prune.


Container-only test elements

Nine elements in 0.5.0 — all evaluated by the agent, like every other element type in OpenSCM. Uniform dispatch, uniform applicability, uniform result lifecycle.

Element Scope Common use
CONTAINER per-host "is a container runtime installed here?" — great as an applicability gate
IMAGE per-container image identity (tag, digest, registry source, name)
NETWORK per-container network mode (host, bridge, none, named)
PRIVILEGED per-container --privileged flag (CIS Docker 5.4)
RUN_USER per-container container's running user (CIS Docker 4.1)
MOUNT per-container bind-mount source paths (CIS Docker 5.5 — block /var/run/docker.sock)
EXPOSED_PORT per-container ports published to the host
READ_ONLY_FS per-container --read-only flag (CIS Docker 5.12)
HEALTH_CHECK per-container HEALTHCHECK directive present

IMAGE

Tests against the container's image reference. Sub-elements:

Sub-element What it returns Example
NAME Image name without tag/registry (e.g. library/nginx) IMAGE NAME contains library
TAG Tag after : (latest if absent) IMAGE TAG not equals latest
DIGEST Pulled image digest (sha256:...) IMAGE DIGEST contains sha256:abc...
SOURCE Registry host (docker.io if implicit) IMAGE SOURCE equals registry.corp.example.com

Image reference parsing follows the standard Docker rules: the first path component is the registry host only when it contains ./:/is localhost; otherwise it's the namespace.

NETWORK

Tests against the container's network configuration. Sub-elements:

Sub-element What it returns Example
MODE host / bridge / none / container:<id> / named network NETWORK MODE not equals host

CONTAINER

Host-level runtime presence check. Unlike IMAGE / NETWORK, this is evaluated by the agent as part of the standard host dispatch — same path as PROCESS, SERVICE, FILE, etc. The agent runs docker --version and podman --version; if either succeeds, the runtime is considered present.

Sub-element What it checks
EXISTS PASS if docker OR podman CLI is on $PATH
NOT EXISTS PASS if neither docker nor podman is on $PATH

Input, condition, and sinput are ignored.

Because CONTAINER is agent-side, it works in the standard applicability section. Add it to a host-level test (CMD / PROCESS / FILE / ...) that should only run on container hosts:

{
  "name": "Docker daemon is hardened",
  "conditions": [
    { "element": "CMD", "input": "docker info | grep ...",
      "selement": "OUTPUT", "condition": "CONTAINS", "sinput": "..." }
  ],
  "applicability": [
    { "element": "CONTAINER", "input": "", "selement": "EXISTS",
      "condition": null, "sinput": null }
  ]
}

On a host without Docker or Podman, applicability fails, the test returns NA, and the report shows "not applicable" rather than a noisy FAIL.

PRIVILEGED

Per-container test for the --privileged flag (CIS Docker 5.4). A privileged container bypasses nearly every kernel isolation property and is functionally equivalent to a root shell on the host.

Sub-element What it checks
EXISTS PASS iff the container was started with --privileged
NOT EXISTS PASS iff the container is not privileged

Input, condition, and sinput are ignored.

RUN_USER

Per-container test for the user the container is running as (CIS Docker 4.1 — don't run as root). Reads Config.User from the inspect output.

Sub-element What it checks
CONTENT The user string — works with EQUALS, NOT EQUALS, CONTAINS, REGEX

Example: RUN_USER CONTENT NOT EQUALS root PASSes when the container runs as anyone other than root.

MOUNT

Per-container test for bind-mount source paths (CIS Docker 5.5 — the most common container-escape misconfiguration is mounting /var/run/docker.sock). Input is the host path to search for; substring match against each mount's src field.

Sub-element What it checks
EXISTS PASS iff any mount's source path contains the input
NOT EXISTS PASS iff no mount source matches

Example: MOUNT NOT EXISTS input='/var/run/docker.sock' PASSes when the Docker socket is not mounted into the container.

EXPOSED_PORT

Per-container test for ports published to the host. Reads NetworkSettings.Ports, which is a JSON map keyed by "port/proto" strings (22/tcp, 443/tcp, 53/udp, …).

Sub-element What it checks
EXISTS PASS iff any exposed port substring-matches the input (e.g. input 22 matches 22/tcp and 22/udp; input 22/tcp only matches that exact entry)
NOT EXISTS PASS iff no exposed port matches
COUNT Numeric condition on the total exposed-port count (input ignored; uses condition + sinput, e.g. COUNT EQUALS 0)

READ_ONLY_FS

Per-container test for the --read-only flag (CIS Docker 5.12 — cheap defence in depth for stateless workloads).

Sub-element What it checks
EXISTS PASS iff the container's root filesystem is read-only
NOT EXISTS PASS iff the root filesystem is writable

Input, condition, and sinput are ignored.

HEALTH_CHECK

Per-container test for the presence of a HEALTHCHECK directive (observability hygiene — containers without HEALTHCHECK can't be restarted on liveness failure by orchestrators).

Sub-element What it checks
EXISTS PASS iff Config.Healthcheck is present in the inspect output
NOT EXISTS PASS iff no HEALTHCHECK is defined

This checks for presence, not for HEALTHCHECK correctness — catches the common "we just forgot" case.


How container tests run

Container tests follow the same path as every other test in OpenSCM:

  1. Run Policy (manual or scheduled) queues entries in the commands table.
  2. The Linux agent picks up the queued tests on its next heartbeat.
  3. For each test, the agent inspects the conditions:
  4. If any condition uses a per-container element (IMAGE, NETWORK), the agent enumerates its local container inventory and evaluates the conditions once per container, sending one result per container identified by the container's runtime ID.
  5. Otherwise (host element like CMD, PROCESS, CONTAINER), the agent evaluates once and sends a single host-level result.
  6. The server's result handler resolves the container's runtime_id to the containers.id it knows about and writes one row in results per result received.

Results appear once the agent's next heartbeat completes — the same lag as any other test in the system.

Result history

Container results live in the same results table as host results, with a non-zero container_id. Host results bind container_id = 0. The primary key is (tenant_id, system_id, test_id, container_id), so per-container verdicts can coexist with the host-level verdict for the same test.

Results survive container churn — if a container is rebuilt with a new ID, its prior results are still queryable for archival reports, rendered as (container removed) in the UI if the row no longer exists in containers.


Canned policy — Container Configuration Hardening L1

The OpenSCM Store ships a starter policy (cis-container-config-l1.json, v1.1.0) with 11 tests covering 8 of the 9 container elements:

Test Element Severity What it checks
Image is not tagged :latest IMAGE TAG Medium Tag pinning
Image source declared (explicit registry host) IMAGE SOURCE Low Supply-chain provenance
Container does not use host network namespace NETWORK MODE High CIS Docker 5.9
Container is not network-isolated (none mode) NETWORK MODE Informational Inventory / drift
Image name does not contain "test" or "dev" IMAGE NAME (×2) Low Promotion-stage drift
Container is not running with --privileged PRIVILEGED NOT EXISTS High CIS Docker 5.4
Container is not running as root RUN_USER CONTENT Medium CIS Docker 4.1
Container does not mount the Docker socket MOUNT NOT EXISTS High CIS Docker 5.5
Container does not expose the SSH port EXPOSED_PORT NOT EXISTS Medium Attack-surface reduction
Container has a read-only root filesystem READ_ONLY_FS EXISTS Low CIS Docker 5.12
Container defines a HEALTHCHECK HEALTH_CHECK EXISTS Informational Observability hygiene

Sync the store under Compliance → Policy Store, import this policy, assign it to a system group, and click Run. Per-container results arrive on the next agent heartbeat — one result per container per test.


Roadmap

Container support shipped in 0.5.0. Releases since then have focused on the surrounding platform rather than new container element types:

  • 0.5.1 — Systems-list rendering fix (container chevron now paints on every host across all pages).
  • 0.5.2Automatic Groups: rule-based membership including container-aware fields (containers_exists, has_runtime, any_container_image).
  • 0.5.3 — Performance pass on group reconciliation and compliance recalc.

Still planned for container scanning itself:

  • Tests inside containers — a CMD element exec path that runs checks via docker exec / podman exec, plus FILE / PACKAGE / USER style probes against container filesystems.
  • Kubernetes — pod inventory and per-pod / per-container tests; kubeconfig handling and RBAC. (Tentatively a future release.)

Current limitations

  • App containers only — Docker and Podman. LXC / LXD inventory deliberately not implemented; install an agent inside the OS container instead.
  • Linux agents only — Docker Desktop on Windows / macOS runs containers inside a hidden VM that the host agent can't reach.
  • No tests inside containers yet — only configuration checks against the cached inventory metadata. CMD-via-docker exec is the next milestone.
  • No mixed-target tests — a single test must be entirely host-targeted or entirely container-targeted.
  • No Kubernetes — pods aren't inventoried; planned for a future release.