docker-host-audit 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- docker_host_audit-0.1.1/PKG-INFO +274 -0
- docker_host_audit-0.1.1/README.md +264 -0
- docker_host_audit-0.1.1/pyproject.toml +63 -0
- docker_host_audit-0.1.1/pyproject.toml.orig +42 -0
- docker_host_audit-0.1.1/src/ca/__init__.py +3 -0
- docker_host_audit-0.1.1/src/ca/__main__.py +6 -0
- docker_host_audit-0.1.1/src/ca/checks/__init__.py +12 -0
- docker_host_audit-0.1.1/src/ca/checks/exposure.py +113 -0
- docker_host_audit-0.1.1/src/ca/checks/hardening.py +100 -0
- docker_host_audit-0.1.1/src/ca/checks/images.py +48 -0
- docker_host_audit-0.1.1/src/ca/checks/privilege.py +146 -0
- docker_host_audit-0.1.1/src/ca/cli.py +114 -0
- docker_host_audit-0.1.1/src/ca/collect.py +102 -0
- docker_host_audit-0.1.1/src/ca/docker_cli.py +79 -0
- docker_host_audit-0.1.1/src/ca/models.py +105 -0
- docker_host_audit-0.1.1/src/ca/py.typed +0 -0
- docker_host_audit-0.1.1/src/ca/redact.py +14 -0
- docker_host_audit-0.1.1/src/ca/report.py +105 -0
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: docker-host-audit
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Read-only security audit for Docker hosts: flags root containers, exposed ports, secrets in env, privileged mode and mounted docker sockets.
|
|
5
|
+
Author: Petr Hromádka
|
|
6
|
+
Author-email: Petr Hromádka <109349500+petrhromadka@users.noreply.github.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
Requires-Python: >=3.12
|
|
9
|
+
Description-Content-Type: text/markdown
|
|
10
|
+
|
|
11
|
+
<div align="center">
|
|
12
|
+
|
|
13
|
+
# 🛡️ container-audit
|
|
14
|
+
|
|
15
|
+
**Read-only security audit for Docker hosts.**
|
|
16
|
+
|
|
17
|
+
Point it at your containers and it reports the risky configuration — root users, exposed ports,
|
|
18
|
+
secrets in environment variables, privileged mode, dangerous capabilities, mounted Docker sockets,
|
|
19
|
+
shared host namespaces, outdated images and missing hardening — as a clean text, JSON or Markdown report.
|
|
20
|
+
|
|
21
|
+
[](https://github.com/petrhromadka/container-audit/actions/workflows/ci.yml)
|
|
22
|
+

|
|
23
|
+

|
|
24
|
+

|
|
25
|
+

|
|
26
|
+
|
|
27
|
+
[Quick start](#-quick-start) · [What it checks](#-what-it-checks) · [Usage](#-usage) · [How it works](#-how-it-works) · [Safety](#-safety)
|
|
28
|
+
|
|
29
|
+
</div>
|
|
30
|
+
|
|
31
|
+
> [!NOTE]
|
|
32
|
+
> **Defensive and read-only.** container-audit only runs `docker ps` and `docker inspect` (or reads
|
|
33
|
+
> a saved copy of that JSON). It never starts, stops, or modifies a container, and it never sends
|
|
34
|
+
> data anywhere.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## 🚀 Quick start
|
|
39
|
+
|
|
40
|
+
No Docker needed to try it — the repo ships a sample `docker inspect` dump:
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
git clone https://github.com/petrhromadka/container-audit
|
|
44
|
+
cd container-audit
|
|
45
|
+
uv run container-audit --input tests/fixtures/sample_inspect.json
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
You'll see eleven findings on the intentionally-insecure `bad-db` container and a clean bill of health
|
|
49
|
+
for the hardened `good-web` one. To scan your own host instead, just run `container-audit` — it
|
|
50
|
+
reads live containers via `docker ps` / `docker inspect`.
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## 🔍 What it checks
|
|
55
|
+
|
|
56
|
+
Severity key: 🔴 critical · 🟠 high · 🟡 medium · 🔵 low. Each finding carries a title, a plain-language
|
|
57
|
+
explanation, short **redacted** evidence, and a one-line remediation.
|
|
58
|
+
|
|
59
|
+
### Privilege & escape
|
|
60
|
+
|
|
61
|
+
| | Check id | What | Why it matters |
|
|
62
|
+
| :-: | --- | --- | --- |
|
|
63
|
+
| 🔴 | `docker-socket-mount` | `/var/run/docker.sock` is mounted in | Socket access is effectively root on the host: it can start any other container. |
|
|
64
|
+
| 🔴 | `privileged` | Container runs `--privileged` | Disables most isolation: full device access and all capabilities. |
|
|
65
|
+
| 🟠 | `root-user` | Runs as root (no `USER`, or `0`/`root`) | A process escape or file-permission bug has root inside the container. |
|
|
66
|
+
| 🟠 | `dangerous-caps` | Caps like `SYS_ADMIN`, `NET_ADMIN`, `SYS_PTRACE` added | Common container-escape building blocks. |
|
|
67
|
+
| 🟠 | `sensitive-host-mount` | Host paths (`/`, `/etc`, `/proc`, `/root`, …) bind-mounted | A compromised container can read or alter host config and credentials. |
|
|
68
|
+
|
|
69
|
+
### Namespaces & exposure
|
|
70
|
+
|
|
71
|
+
| | Check id | What | Why it matters |
|
|
72
|
+
| :-: | --- | --- | --- |
|
|
73
|
+
| 🟠 | `host-pid` | Shares the host PID namespace (`--pid=host`) | It can see and signal every process on the host. |
|
|
74
|
+
| 🟠 | `host-network` | Shares the host network namespace (`--network=host`) | Network isolation and port mapping are bypassed. |
|
|
75
|
+
| 🟡 | `port-all-interfaces` | Port published on `0.0.0.0` / `::` | Reachable from every network the host is on, not just where you intend. |
|
|
76
|
+
| 🟡 | `host-ipc` | Shares the host IPC namespace (`--ipc=host`) | Shared memory and semaphores cross the container boundary. |
|
|
77
|
+
| 🔵 | `host-uts` | Shares the host UTS namespace (`--uts=host`) | It can change the host's hostname. |
|
|
78
|
+
|
|
79
|
+
### Secrets & hardening
|
|
80
|
+
|
|
81
|
+
| | Check id | What | Why it matters |
|
|
82
|
+
| :-: | --- | --- | --- |
|
|
83
|
+
| 🟠 | `secret-in-env` | Env var that looks like a secret, or a URL with embedded credentials | Secrets in env vars leak via `docker inspect`, logs and child processes. |
|
|
84
|
+
| 🟡 | `no-new-privileges-missing` | `no-new-privileges:true` is not set | setuid binaries inside the container can still escalate privileges. |
|
|
85
|
+
| 🔵 | `writable-rootfs` | Root filesystem is writable (not `read_only`) | An attacker can persist changes and tooling. |
|
|
86
|
+
| 🟡 | `stale-image` | Image was built more than 180 days ago | Old images usually miss security patches released since; rebuild or pull a current one. |
|
|
87
|
+
| 🔵 | `unpinned-image` | Image uses `:latest`, no tag, or no digest | Pulls aren't reproducible; upstream can silently change what runs. |
|
|
88
|
+
| 🔵 | `unlimited-memory` | No memory limit set | A single container can exhaust host RAM and take the host down. |
|
|
89
|
+
|
|
90
|
+
---
|
|
91
|
+
|
|
92
|
+
## 📦 Install
|
|
93
|
+
|
|
94
|
+
Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/). The `docker` CLI is only needed to audit
|
|
95
|
+
a live host; auditing a saved file with `--input` does not need it.
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
uv tool install . # install as a tool from a checkout
|
|
99
|
+
uvx --from . container-audit # or run without installing
|
|
100
|
+
uv run container-audit # or run inside the project environment
|
|
101
|
+
uv run python -m ca # module form (use this if the console script is blocked)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
> On Windows, Smart App Control may block the generated `container-audit.exe` launcher. Run the
|
|
105
|
+
> tool as a module instead: `python -m ca` (or `uv run python -m ca`).
|
|
106
|
+
|
|
107
|
+
Once published to PyPI: `pipx install docker-host-audit` (then run `container-audit`), or `uvx docker-host-audit`.
|
|
108
|
+
|
|
109
|
+
### Run with Docker
|
|
110
|
+
|
|
111
|
+
The image ships the tool (the Docker CLI is intentionally not bundled); feed it a saved inspect dump
|
|
112
|
+
on stdin:
|
|
113
|
+
|
|
114
|
+
```bash
|
|
115
|
+
docker build -t container-audit .
|
|
116
|
+
docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) | docker run --rm -i container-audit --input /dev/stdin --fail-on high
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
---
|
|
120
|
+
|
|
121
|
+
## 💻 Usage
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
container-audit # audit running containers
|
|
125
|
+
container-audit --include-stopped # include stopped ones too
|
|
126
|
+
container-audit --format json # machine-readable output
|
|
127
|
+
container-audit --format markdown > report.md # for an issue / PR / wiki
|
|
128
|
+
|
|
129
|
+
docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) > sample.json # save containers + their images
|
|
130
|
+
container-audit --input sample.json --fail-on high # audit the dump; non-zero exit for CI
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
| Flag | Meaning |
|
|
134
|
+
| --- | --- |
|
|
135
|
+
| `--input FILE` | Read `docker inspect` JSON from `FILE` instead of calling Docker. |
|
|
136
|
+
| `--format FORMAT` | Output format: `text` (default), `json` or `markdown`. |
|
|
137
|
+
| `--include-stopped` | Also inspect containers that are not running. |
|
|
138
|
+
| `--include-host` | Include the real hostname in the report (off by default, so output is safe to share). |
|
|
139
|
+
| `--min-severity LEVEL` | Hide findings below `LEVEL` (`info`, `low`, `medium`, `high`, `critical`). Default `info` (show all). |
|
|
140
|
+
| `--fail-on LEVEL` | Exit non-zero if any shown finding is at or above `LEVEL` (`none`, `low`, `medium`, `high`, `critical`). Default `none`. |
|
|
141
|
+
| `--version` | Print the version and exit. |
|
|
142
|
+
|
|
143
|
+
**Exit codes:** `0` clean (nothing reached `--fail-on`) · `1` a finding met `--fail-on` · `2` could
|
|
144
|
+
not run (Docker unavailable, or a missing/invalid `--input` file).
|
|
145
|
+
|
|
146
|
+
Image age comes from `docker inspect --type image`. If an image was deleted while its container
|
|
147
|
+
still runs, or the dump has no image objects, the age is unknown and `stale-image` skips that
|
|
148
|
+
container. Images from reproducible builds (distroless, ko, Nix) carry a fixed 1970 date and are
|
|
149
|
+
skipped too.
|
|
150
|
+
|
|
151
|
+
### Coolify and other self-hosted PaaS
|
|
152
|
+
|
|
153
|
+
It works unchanged on a [Coolify](https://coolify.io) host. Expect `docker-socket-mount` on Coolify's
|
|
154
|
+
own `coolify-proxy` (Traefik) and `coolify-sentinel` containers: they need the socket by design. Review
|
|
155
|
+
everything else, especially apps you deployed from public templates, which often publish database
|
|
156
|
+
ports on `0.0.0.0` and keep passwords in environment variables.
|
|
157
|
+
|
|
158
|
+
### Use in CI
|
|
159
|
+
|
|
160
|
+
```yaml
|
|
161
|
+
- name: Audit containers
|
|
162
|
+
run: |
|
|
163
|
+
docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) > containers.json
|
|
164
|
+
uvx --from git+https://github.com/petrhromadka/container-audit \
|
|
165
|
+
container-audit --input containers.json --fail-on high
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## ⚙️ How it works
|
|
171
|
+
|
|
172
|
+
```text
|
|
173
|
+
docker ps / docker inspect -> parse into Container objects -> run checks -> render report
|
|
174
|
+
(or --input file) (pure functions) text / json / markdown
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
1. **Collect** — list containers with `docker ps` and read their config with `docker inspect` (or
|
|
178
|
+
parse a saved copy via `--input`).
|
|
179
|
+
2. **Check** — each check is a pure function over the parsed containers that yields findings. Checks
|
|
180
|
+
never talk to Docker and never change anything.
|
|
181
|
+
3. **Report** — findings are sorted by severity and rendered as text, JSON or Markdown; with
|
|
182
|
+
`--fail-on`, the exit code reflects the worst severity found.
|
|
183
|
+
|
|
184
|
+
<details>
|
|
185
|
+
<summary>📋 Example report (from the bundled sample fixture)</summary>
|
|
186
|
+
|
|
187
|
+
```text
|
|
188
|
+
container-audit report
|
|
189
|
+
Host: (redacted)
|
|
190
|
+
Scanned: 2 container(s)
|
|
191
|
+
Timestamp: 2026-11-05T19:32:19+00:00
|
|
192
|
+
Findings: 11 (critical=2, high=3, medium=4, low=2, info=0)
|
|
193
|
+
|
|
194
|
+
[CRITICAL] docker-socket-mount bad-db - Access to the Docker socket grants control of the Docker daemon, and therefore of the host.
|
|
195
|
+
evidence: /var/run/docker.sock -> /var/run/docker.sock (rw)
|
|
196
|
+
remediation: Remove the socket mount or use a restricted docker-socket-proxy.
|
|
197
|
+
[CRITICAL] privileged bad-db - Privileged mode grants all capabilities and host device access, so a compromise is effectively a host compromise.
|
|
198
|
+
evidence: HostConfig.Privileged=true
|
|
199
|
+
remediation: Remove `privileged: true`; add only the capabilities needed.
|
|
200
|
+
[HIGH] dangerous-caps bad-db - These capabilities allow administrative or host-affecting operations and weaken container isolation.
|
|
201
|
+
evidence: cap_add: SYS_ADMIN
|
|
202
|
+
remediation: Remove these from `cap_add`; grant only what is required.
|
|
203
|
+
[HIGH] root-user bad-db - The main process runs as UID 0, widening the impact of an escape.
|
|
204
|
+
evidence: Config.User=''
|
|
205
|
+
remediation: Set a non-root USER in the image or `user:` in compose.
|
|
206
|
+
[HIGH] secret-in-env bad-db - Environment variable DB_PASSWORD looks like a secret and has a literal value; it is visible via docker inspect and to anyone who can read the container configuration.
|
|
207
|
+
evidence: DB_PASSWORD=***
|
|
208
|
+
remediation: Use Docker secrets or an env file kept outside the image.
|
|
209
|
+
[MEDIUM] host-ipc bad-db - The container can access host shared memory and IPC objects, weakening isolation.
|
|
210
|
+
evidence: HostConfig.IpcMode=host
|
|
211
|
+
remediation: Remove `ipc: host`; do not share the host IPC namespace.
|
|
212
|
+
[MEDIUM] no-new-privileges-missing bad-db - Without no-new-privileges, setuid binaries inside the container can escalate privileges.
|
|
213
|
+
evidence: HostConfig.SecurityOpt lacks no-new-privileges
|
|
214
|
+
remediation: Add `security_opt: ["no-new-privileges:true"]`.
|
|
215
|
+
[MEDIUM] port-all-interfaces bad-db - The port is reachable from every network the host is attached to.
|
|
216
|
+
evidence: 0.0.0.0:5432 -> 5432/tcp
|
|
217
|
+
remediation: Bind to 127.0.0.1 (e.g. 127.0.0.1:<port>:<port>) unless public.
|
|
218
|
+
[MEDIUM] stale-image bad-db - The image is 1390 days old, so it likely lacks security patches released since it was built.
|
|
219
|
+
evidence: image built 2023-01-15 (1390 days ago)
|
|
220
|
+
remediation: Pull or rebuild the image on a current base and recreate the container; automate updates (e.g. Renovate, Dependabot).
|
|
221
|
+
[LOW] unlimited-memory bad-db - With no memory limit, one container can exhaust the host's RAM.
|
|
222
|
+
evidence: HostConfig.Memory=0
|
|
223
|
+
remediation: Set a memory limit (`mem_limit` in compose).
|
|
224
|
+
[LOW] writable-rootfs bad-db - A writable root filesystem lets an attacker persist changes.
|
|
225
|
+
evidence: HostConfig.ReadonlyRootfs=false
|
|
226
|
+
remediation: Set `read_only: true` and use tmpfs or volumes for writable paths.
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The second container, `good-web`, is hardened (non-root, read-only rootfs, memory limit,
|
|
230
|
+
`no-new-privileges`, port bound to `127.0.0.1`) and produces no findings.
|
|
231
|
+
|
|
232
|
+
</details>
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## 🔒 Safety
|
|
237
|
+
|
|
238
|
+
- **Read-only.** The only Docker commands used are `docker ps` and `docker inspect`. Nothing is
|
|
239
|
+
started, stopped, restarted, or reconfigured.
|
|
240
|
+
- **Secret values are never printed.** For env vars that look like secrets, only the variable
|
|
241
|
+
**name** is reported, with the value masked to `***` — no characters of the value appear in any
|
|
242
|
+
output format.
|
|
243
|
+
- **Hostname hidden by default.** Reports show `(redacted)` as the host unless you pass
|
|
244
|
+
`--include-host`.
|
|
245
|
+
- **Output is sanitized.** Control and terminal-escape characters from container metadata are
|
|
246
|
+
stripped, so a report is safe to print in a terminal or paste into Markdown.
|
|
247
|
+
- **No network use, no runtime dependencies.** The tool never transmits data anywhere.
|
|
248
|
+
- Reports can still reveal your container names, image names, ports and mount paths — review one
|
|
249
|
+
before sharing it publicly.
|
|
250
|
+
|
|
251
|
+
---
|
|
252
|
+
|
|
253
|
+
## 🧰 Development
|
|
254
|
+
|
|
255
|
+
```bash
|
|
256
|
+
uv sync # create the environment
|
|
257
|
+
uv run ruff check . # lint
|
|
258
|
+
uv run ruff format --check . # formatting
|
|
259
|
+
uv run ty check src # type check
|
|
260
|
+
uv run pytest # tests
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Pre-commit hooks (ruff, gitleaks, whitespace fixes) live in `.pre-commit-config.yaml`; install them
|
|
264
|
+
with [`prek`](https://github.com/j178/prek) or `pre-commit`. CI runs the same checks plus a gitleaks
|
|
265
|
+
scan over the full history.
|
|
266
|
+
|
|
267
|
+
To add a check, write a function that takes `list[Container]` and yields `Finding` objects, then
|
|
268
|
+
register it in the `CHECKS` list of the relevant module under `src/ca/checks/`.
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## 📄 License
|
|
273
|
+
|
|
274
|
+
[MIT](LICENSE) © 2026 Petr Hromádka
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
<div align="center">
|
|
2
|
+
|
|
3
|
+
# 🛡️ container-audit
|
|
4
|
+
|
|
5
|
+
**Read-only security audit for Docker hosts.**
|
|
6
|
+
|
|
7
|
+
Point it at your containers and it reports the risky configuration — root users, exposed ports,
|
|
8
|
+
secrets in environment variables, privileged mode, dangerous capabilities, mounted Docker sockets,
|
|
9
|
+
shared host namespaces, outdated images and missing hardening — as a clean text, JSON or Markdown report.
|
|
10
|
+
|
|
11
|
+
[](https://github.com/petrhromadka/container-audit/actions/workflows/ci.yml)
|
|
12
|
+

|
|
13
|
+

|
|
14
|
+

|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
[Quick start](#-quick-start) · [What it checks](#-what-it-checks) · [Usage](#-usage) · [How it works](#-how-it-works) · [Safety](#-safety)
|
|
18
|
+
|
|
19
|
+
</div>
|
|
20
|
+
|
|
21
|
+
> [!NOTE]
|
|
22
|
+
> **Defensive and read-only.** container-audit only runs `docker ps` and `docker inspect` (or reads
|
|
23
|
+
> a saved copy of that JSON). It never starts, stops, or modifies a container, and it never sends
|
|
24
|
+
> data anywhere.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## 🚀 Quick start
|
|
29
|
+
|
|
30
|
+
No Docker needed to try it — the repo ships a sample `docker inspect` dump:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
git clone https://github.com/petrhromadka/container-audit
|
|
34
|
+
cd container-audit
|
|
35
|
+
uv run container-audit --input tests/fixtures/sample_inspect.json
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
You'll see eleven findings on the intentionally-insecure `bad-db` container and a clean bill of health
|
|
39
|
+
for the hardened `good-web` one. To scan your own host instead, just run `container-audit` — it
|
|
40
|
+
reads live containers via `docker ps` / `docker inspect`.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 🔍 What it checks
|
|
45
|
+
|
|
46
|
+
Severity key: 🔴 critical · 🟠 high · 🟡 medium · 🔵 low. Each finding carries a title, a plain-language
|
|
47
|
+
explanation, short **redacted** evidence, and a one-line remediation.
|
|
48
|
+
|
|
49
|
+
### Privilege & escape
|
|
50
|
+
|
|
51
|
+
| | Check id | What | Why it matters |
|
|
52
|
+
| :-: | --- | --- | --- |
|
|
53
|
+
| 🔴 | `docker-socket-mount` | `/var/run/docker.sock` is mounted in | Socket access is effectively root on the host: it can start any other container. |
|
|
54
|
+
| 🔴 | `privileged` | Container runs `--privileged` | Disables most isolation: full device access and all capabilities. |
|
|
55
|
+
| 🟠 | `root-user` | Runs as root (no `USER`, or `0`/`root`) | A process escape or file-permission bug has root inside the container. |
|
|
56
|
+
| 🟠 | `dangerous-caps` | Caps like `SYS_ADMIN`, `NET_ADMIN`, `SYS_PTRACE` added | Common container-escape building blocks. |
|
|
57
|
+
| 🟠 | `sensitive-host-mount` | Host paths (`/`, `/etc`, `/proc`, `/root`, …) bind-mounted | A compromised container can read or alter host config and credentials. |
|
|
58
|
+
|
|
59
|
+
### Namespaces & exposure
|
|
60
|
+
|
|
61
|
+
| | Check id | What | Why it matters |
|
|
62
|
+
| :-: | --- | --- | --- |
|
|
63
|
+
| 🟠 | `host-pid` | Shares the host PID namespace (`--pid=host`) | It can see and signal every process on the host. |
|
|
64
|
+
| 🟠 | `host-network` | Shares the host network namespace (`--network=host`) | Network isolation and port mapping are bypassed. |
|
|
65
|
+
| 🟡 | `port-all-interfaces` | Port published on `0.0.0.0` / `::` | Reachable from every network the host is on, not just where you intend. |
|
|
66
|
+
| 🟡 | `host-ipc` | Shares the host IPC namespace (`--ipc=host`) | Shared memory and semaphores cross the container boundary. |
|
|
67
|
+
| 🔵 | `host-uts` | Shares the host UTS namespace (`--uts=host`) | It can change the host's hostname. |
|
|
68
|
+
|
|
69
|
+
### Secrets & hardening
|
|
70
|
+
|
|
71
|
+
| | Check id | What | Why it matters |
|
|
72
|
+
| :-: | --- | --- | --- |
|
|
73
|
+
| 🟠 | `secret-in-env` | Env var that looks like a secret, or a URL with embedded credentials | Secrets in env vars leak via `docker inspect`, logs and child processes. |
|
|
74
|
+
| 🟡 | `no-new-privileges-missing` | `no-new-privileges:true` is not set | setuid binaries inside the container can still escalate privileges. |
|
|
75
|
+
| 🔵 | `writable-rootfs` | Root filesystem is writable (not `read_only`) | An attacker can persist changes and tooling. |
|
|
76
|
+
| 🟡 | `stale-image` | Image was built more than 180 days ago | Old images usually miss security patches released since; rebuild or pull a current one. |
|
|
77
|
+
| 🔵 | `unpinned-image` | Image uses `:latest`, no tag, or no digest | Pulls aren't reproducible; upstream can silently change what runs. |
|
|
78
|
+
| 🔵 | `unlimited-memory` | No memory limit set | A single container can exhaust host RAM and take the host down. |
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## 📦 Install
|
|
83
|
+
|
|
84
|
+
Requires Python 3.12+ and [uv](https://docs.astral.sh/uv/). The `docker` CLI is only needed to audit
|
|
85
|
+
a live host; auditing a saved file with `--input` does not need it.
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
uv tool install . # install as a tool from a checkout
|
|
89
|
+
uvx --from . container-audit # or run without installing
|
|
90
|
+
uv run container-audit # or run inside the project environment
|
|
91
|
+
uv run python -m ca # module form (use this if the console script is blocked)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
> On Windows, Smart App Control may block the generated `container-audit.exe` launcher. Run the
|
|
95
|
+
> tool as a module instead: `python -m ca` (or `uv run python -m ca`).
|
|
96
|
+
|
|
97
|
+
Once published to PyPI: `pipx install docker-host-audit` (then run `container-audit`), or `uvx docker-host-audit`.
|
|
98
|
+
|
|
99
|
+
### Run with Docker
|
|
100
|
+
|
|
101
|
+
The image ships the tool (the Docker CLI is intentionally not bundled); feed it a saved inspect dump
|
|
102
|
+
on stdin:
|
|
103
|
+
|
|
104
|
+
```bash
|
|
105
|
+
docker build -t container-audit .
|
|
106
|
+
docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) | docker run --rm -i container-audit --input /dev/stdin --fail-on high
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## 💻 Usage
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
container-audit # audit running containers
|
|
115
|
+
container-audit --include-stopped # include stopped ones too
|
|
116
|
+
container-audit --format json # machine-readable output
|
|
117
|
+
container-audit --format markdown > report.md # for an issue / PR / wiki
|
|
118
|
+
|
|
119
|
+
docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) > sample.json # save containers + their images
|
|
120
|
+
container-audit --input sample.json --fail-on high # audit the dump; non-zero exit for CI
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
| Flag | Meaning |
|
|
124
|
+
| --- | --- |
|
|
125
|
+
| `--input FILE` | Read `docker inspect` JSON from `FILE` instead of calling Docker. |
|
|
126
|
+
| `--format FORMAT` | Output format: `text` (default), `json` or `markdown`. |
|
|
127
|
+
| `--include-stopped` | Also inspect containers that are not running. |
|
|
128
|
+
| `--include-host` | Include the real hostname in the report (off by default, so output is safe to share). |
|
|
129
|
+
| `--min-severity LEVEL` | Hide findings below `LEVEL` (`info`, `low`, `medium`, `high`, `critical`). Default `info` (show all). |
|
|
130
|
+
| `--fail-on LEVEL` | Exit non-zero if any shown finding is at or above `LEVEL` (`none`, `low`, `medium`, `high`, `critical`). Default `none`. |
|
|
131
|
+
| `--version` | Print the version and exit. |
|
|
132
|
+
|
|
133
|
+
**Exit codes:** `0` clean (nothing reached `--fail-on`) · `1` a finding met `--fail-on` · `2` could
|
|
134
|
+
not run (Docker unavailable, or a missing/invalid `--input` file).
|
|
135
|
+
|
|
136
|
+
Image age comes from `docker inspect --type image`. If an image was deleted while its container
|
|
137
|
+
still runs, or the dump has no image objects, the age is unknown and `stale-image` skips that
|
|
138
|
+
container. Images from reproducible builds (distroless, ko, Nix) carry a fixed 1970 date and are
|
|
139
|
+
skipped too.
|
|
140
|
+
|
|
141
|
+
### Coolify and other self-hosted PaaS
|
|
142
|
+
|
|
143
|
+
It works unchanged on a [Coolify](https://coolify.io) host. Expect `docker-socket-mount` on Coolify's
|
|
144
|
+
own `coolify-proxy` (Traefik) and `coolify-sentinel` containers: they need the socket by design. Review
|
|
145
|
+
everything else, especially apps you deployed from public templates, which often publish database
|
|
146
|
+
ports on `0.0.0.0` and keep passwords in environment variables.
|
|
147
|
+
|
|
148
|
+
### Use in CI
|
|
149
|
+
|
|
150
|
+
```yaml
|
|
151
|
+
- name: Audit containers
|
|
152
|
+
run: |
|
|
153
|
+
docker inspect $(docker ps -aq) $(docker inspect -f '{{.Image}}' $(docker ps -aq) | sort -u) > containers.json
|
|
154
|
+
uvx --from git+https://github.com/petrhromadka/container-audit \
|
|
155
|
+
container-audit --input containers.json --fail-on high
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## ⚙️ How it works
|
|
161
|
+
|
|
162
|
+
```text
|
|
163
|
+
docker ps / docker inspect -> parse into Container objects -> run checks -> render report
|
|
164
|
+
(or --input file) (pure functions) text / json / markdown
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
1. **Collect** — list containers with `docker ps` and read their config with `docker inspect` (or
|
|
168
|
+
parse a saved copy via `--input`).
|
|
169
|
+
2. **Check** — each check is a pure function over the parsed containers that yields findings. Checks
|
|
170
|
+
never talk to Docker and never change anything.
|
|
171
|
+
3. **Report** — findings are sorted by severity and rendered as text, JSON or Markdown; with
|
|
172
|
+
`--fail-on`, the exit code reflects the worst severity found.
|
|
173
|
+
|
|
174
|
+
<details>
|
|
175
|
+
<summary>📋 Example report (from the bundled sample fixture)</summary>
|
|
176
|
+
|
|
177
|
+
```text
|
|
178
|
+
container-audit report
|
|
179
|
+
Host: (redacted)
|
|
180
|
+
Scanned: 2 container(s)
|
|
181
|
+
Timestamp: 2026-11-05T19:32:19+00:00
|
|
182
|
+
Findings: 11 (critical=2, high=3, medium=4, low=2, info=0)
|
|
183
|
+
|
|
184
|
+
[CRITICAL] docker-socket-mount bad-db - Access to the Docker socket grants control of the Docker daemon, and therefore of the host.
|
|
185
|
+
evidence: /var/run/docker.sock -> /var/run/docker.sock (rw)
|
|
186
|
+
remediation: Remove the socket mount or use a restricted docker-socket-proxy.
|
|
187
|
+
[CRITICAL] privileged bad-db - Privileged mode grants all capabilities and host device access, so a compromise is effectively a host compromise.
|
|
188
|
+
evidence: HostConfig.Privileged=true
|
|
189
|
+
remediation: Remove `privileged: true`; add only the capabilities needed.
|
|
190
|
+
[HIGH] dangerous-caps bad-db - These capabilities allow administrative or host-affecting operations and weaken container isolation.
|
|
191
|
+
evidence: cap_add: SYS_ADMIN
|
|
192
|
+
remediation: Remove these from `cap_add`; grant only what is required.
|
|
193
|
+
[HIGH] root-user bad-db - The main process runs as UID 0, widening the impact of an escape.
|
|
194
|
+
evidence: Config.User=''
|
|
195
|
+
remediation: Set a non-root USER in the image or `user:` in compose.
|
|
196
|
+
[HIGH] secret-in-env bad-db - Environment variable DB_PASSWORD looks like a secret and has a literal value; it is visible via docker inspect and to anyone who can read the container configuration.
|
|
197
|
+
evidence: DB_PASSWORD=***
|
|
198
|
+
remediation: Use Docker secrets or an env file kept outside the image.
|
|
199
|
+
[MEDIUM] host-ipc bad-db - The container can access host shared memory and IPC objects, weakening isolation.
|
|
200
|
+
evidence: HostConfig.IpcMode=host
|
|
201
|
+
remediation: Remove `ipc: host`; do not share the host IPC namespace.
|
|
202
|
+
[MEDIUM] no-new-privileges-missing bad-db - Without no-new-privileges, setuid binaries inside the container can escalate privileges.
|
|
203
|
+
evidence: HostConfig.SecurityOpt lacks no-new-privileges
|
|
204
|
+
remediation: Add `security_opt: ["no-new-privileges:true"]`.
|
|
205
|
+
[MEDIUM] port-all-interfaces bad-db - The port is reachable from every network the host is attached to.
|
|
206
|
+
evidence: 0.0.0.0:5432 -> 5432/tcp
|
|
207
|
+
remediation: Bind to 127.0.0.1 (e.g. 127.0.0.1:<port>:<port>) unless public.
|
|
208
|
+
[MEDIUM] stale-image bad-db - The image is 1390 days old, so it likely lacks security patches released since it was built.
|
|
209
|
+
evidence: image built 2023-01-15 (1390 days ago)
|
|
210
|
+
remediation: Pull or rebuild the image on a current base and recreate the container; automate updates (e.g. Renovate, Dependabot).
|
|
211
|
+
[LOW] unlimited-memory bad-db - With no memory limit, one container can exhaust the host's RAM.
|
|
212
|
+
evidence: HostConfig.Memory=0
|
|
213
|
+
remediation: Set a memory limit (`mem_limit` in compose).
|
|
214
|
+
[LOW] writable-rootfs bad-db - A writable root filesystem lets an attacker persist changes.
|
|
215
|
+
evidence: HostConfig.ReadonlyRootfs=false
|
|
216
|
+
remediation: Set `read_only: true` and use tmpfs or volumes for writable paths.
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The second container, `good-web`, is hardened (non-root, read-only rootfs, memory limit,
|
|
220
|
+
`no-new-privileges`, port bound to `127.0.0.1`) and produces no findings.
|
|
221
|
+
|
|
222
|
+
</details>
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## 🔒 Safety
|
|
227
|
+
|
|
228
|
+
- **Read-only.** The only Docker commands used are `docker ps` and `docker inspect`. Nothing is
|
|
229
|
+
started, stopped, restarted, or reconfigured.
|
|
230
|
+
- **Secret values are never printed.** For env vars that look like secrets, only the variable
|
|
231
|
+
**name** is reported, with the value masked to `***` — no characters of the value appear in any
|
|
232
|
+
output format.
|
|
233
|
+
- **Hostname hidden by default.** Reports show `(redacted)` as the host unless you pass
|
|
234
|
+
`--include-host`.
|
|
235
|
+
- **Output is sanitized.** Control and terminal-escape characters from container metadata are
|
|
236
|
+
stripped, so a report is safe to print in a terminal or paste into Markdown.
|
|
237
|
+
- **No network use, no runtime dependencies.** The tool never transmits data anywhere.
|
|
238
|
+
- Reports can still reveal your container names, image names, ports and mount paths — review one
|
|
239
|
+
before sharing it publicly.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## 🧰 Development
|
|
244
|
+
|
|
245
|
+
```bash
|
|
246
|
+
uv sync # create the environment
|
|
247
|
+
uv run ruff check . # lint
|
|
248
|
+
uv run ruff format --check . # formatting
|
|
249
|
+
uv run ty check src # type check
|
|
250
|
+
uv run pytest # tests
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Pre-commit hooks (ruff, gitleaks, whitespace fixes) live in `.pre-commit-config.yaml`; install them
|
|
254
|
+
with [`prek`](https://github.com/j178/prek) or `pre-commit`. CI runs the same checks plus a gitleaks
|
|
255
|
+
scan over the full history.
|
|
256
|
+
|
|
257
|
+
To add a check, write a function that takes `list[Container]` and yields `Finding` objects, then
|
|
258
|
+
register it in the `CHECKS` list of the relevant module under `src/ca/checks/`.
|
|
259
|
+
|
|
260
|
+
---
|
|
261
|
+
|
|
262
|
+
## 📄 License
|
|
263
|
+
|
|
264
|
+
[MIT](LICENSE) © 2026 Petr Hromádka
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "docker-host-audit"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Read-only security audit for Docker hosts: flags root containers, exposed ports, secrets in env, privileged mode and mounted docker sockets."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
requires-python = ">=3.12"
|
|
8
|
+
dependencies = []
|
|
9
|
+
|
|
10
|
+
[[project.authors]]
|
|
11
|
+
name = "Petr Hromádka"
|
|
12
|
+
email = "109349500+petrhromadka@users.noreply.github.com"
|
|
13
|
+
|
|
14
|
+
[project.scripts]
|
|
15
|
+
container-audit = "ca.cli:main"
|
|
16
|
+
docker-host-audit = "ca.cli:main"
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build>=0.12.23,<0.13.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[tool.uv.build-backend]
|
|
23
|
+
module-name = "ca"
|
|
24
|
+
|
|
25
|
+
[tool.ruff]
|
|
26
|
+
line-length = 100
|
|
27
|
+
target-version = "py312"
|
|
28
|
+
|
|
29
|
+
[tool.ruff.lint]
|
|
30
|
+
select = [
|
|
31
|
+
"E",
|
|
32
|
+
"F",
|
|
33
|
+
"W",
|
|
34
|
+
"I",
|
|
35
|
+
"B",
|
|
36
|
+
"UP",
|
|
37
|
+
"SIM",
|
|
38
|
+
"RUF",
|
|
39
|
+
"S",
|
|
40
|
+
"PTH",
|
|
41
|
+
]
|
|
42
|
+
ignore = [
|
|
43
|
+
"S603",
|
|
44
|
+
"S607",
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.ruff.lint.per-file-ignores]
|
|
48
|
+
"tests/**" = [
|
|
49
|
+
"S101",
|
|
50
|
+
"S105",
|
|
51
|
+
"S106",
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
[tool.pytest.ini_options]
|
|
55
|
+
testpaths = ["tests"]
|
|
56
|
+
addopts = "-q"
|
|
57
|
+
|
|
58
|
+
[dependency-groups]
|
|
59
|
+
dev = [
|
|
60
|
+
"pytest>=9.2.0",
|
|
61
|
+
"ruff>=0.17.4",
|
|
62
|
+
"ty>=0.0.98",
|
|
63
|
+
]
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "docker-host-audit"
|
|
3
|
+
version = "0.1.1"
|
|
4
|
+
description = "Read-only security audit for Docker hosts: flags root containers, exposed ports, secrets in env, privileged mode and mounted docker sockets."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
license = "MIT"
|
|
7
|
+
authors = [{ name = "Petr Hromádka", email = "109349500+petrhromadka@users.noreply.github.com" }]
|
|
8
|
+
requires-python = ">=3.12"
|
|
9
|
+
dependencies = []
|
|
10
|
+
|
|
11
|
+
[project.scripts]
|
|
12
|
+
container-audit = "ca.cli:main"
|
|
13
|
+
docker-host-audit = "ca.cli:main"
|
|
14
|
+
|
|
15
|
+
[build-system]
|
|
16
|
+
requires = ["uv_build>=0.12.23,<0.13.0"]
|
|
17
|
+
build-backend = "uv_build"
|
|
18
|
+
|
|
19
|
+
[tool.uv.build-backend]
|
|
20
|
+
module-name = "ca"
|
|
21
|
+
|
|
22
|
+
[tool.ruff]
|
|
23
|
+
line-length = 100
|
|
24
|
+
target-version = "py312"
|
|
25
|
+
|
|
26
|
+
[tool.ruff.lint]
|
|
27
|
+
select = ["E", "F", "W", "I", "B", "UP", "SIM", "RUF", "S", "PTH"]
|
|
28
|
+
ignore = ["S603", "S607"]
|
|
29
|
+
|
|
30
|
+
[tool.ruff.lint.per-file-ignores]
|
|
31
|
+
"tests/**" = ["S101", "S105", "S106"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
35
|
+
addopts = "-q"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
dev = [
|
|
39
|
+
"pytest>=9.2.0",
|
|
40
|
+
"ruff>=0.17.4",
|
|
41
|
+
"ty>=0.0.98",
|
|
42
|
+
]
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"""Check registry. Each submodule exposes `CHECKS: list[CheckFn]`; we aggregate them."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from ca.models import CheckFn
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def get_checks() -> list[CheckFn]:
|
|
9
|
+
"""Every registered check, across all submodules."""
|
|
10
|
+
from ca.checks import exposure, hardening, images, privilege
|
|
11
|
+
|
|
12
|
+
return [*privilege.CHECKS, *exposure.CHECKS, *hardening.CHECKS, *images.CHECKS]
|