Security policy #
Reporting a vulnerability #
Report privately through GitHub's advisory workflow: https://github.com/kogunlowo123/llmops-observability/security/advisories/new
Please do not open a public issue for a vulnerability.
Include what you have: the window, objectives file or command that triggers it, what you expected, what happened, and the version or commit. A proof of concept helps but is not required.
What to expect. This is a portfolio project maintained by one person, not a funded product, and it is fairer to say so than to publish a service-level agreement nobody is on call for. Acknowledgement within a week is realistic. If a report is valid it will be fixed and credited, and the fix will say what was wrong rather than describing it as "hardening".
Supported versions #
The main branch. There is no backport policy for older tags.
What this protects, and what it does not #
THREAT-MODEL.md is the document to read before running this on anything that matters. The short version:
In scope. Prompt or completion text reaching telemetry; credentials reaching a report through the attribute bag; unintended network egress; resource exhaustion through a hostile window file, including a decompression bomb; a gate that silently stops being able to fail; an unevaluated rule being reported as a passing one.
Out of scope. Telemetry that is wrong rather than malformed — there is no
cryptographic provenance on a span and no attempt at one. An attacker who can
already modify the objectives file in your repository; they do not need a
vulnerability. The security of the collector you point --allow-network at.
The things most worth understanding #
A span has no prompt or completion field. Not a redaction rule — there is no
field. extra="forbid" means an attempt to add one is refused at validation.
There is no configuration under which this tool exports a prompt, because there
is nowhere to put one. Redaction over the attribute bag is a second line of
defence against a credential arriving through it, not the only line.
Nothing reaches the network unless it is asked for. An exporter that opens a
socket cannot be constructed without --allow-network. Not "declines to
send" — the object does not exist in the process. An LLMOPS_EXPORT__OTLP_ENDPOINT
left in a CI job by mistake cannot start sending on its own.
Compressed input is bounded. Accepting .gz windows means accepting the
possibility of a decompression bomb, so the size limit is applied to the
decompressed stream and not only to the file on disk. Without that guard a
200 KB file could expand until the runner is killed — which reaches an operator
as "the gate is flaky" rather than as an attack.
A rule that was not evaluated is never reported as passing. Under-sampled
and uncovered rules get their own section in every report format, including a
skipped case in the JUnit XML. "We did not look" and "we looked and it was
fine" are different facts, and a gate that conflates them is worse than no gate
because it manufactures confidence.
Credentials #
This project handles none of its own. It reads telemetry files and writes
reports; there is no provider client and no authentication anywhere in the
codebase. .env.example contains no credential field, because there is nothing
for one to configure.
Never commit a populated .env. It is gitignored, and CI runs gitleaks over
both the working tree and the full git history — not just the diff — on
every push, because a secret removed in a later commit is still in the history.
What CI enforces #
| Check | Tool | Scope |
|---|---|---|
| Secrets | gitleaks | Working tree and full history |
| Static analysis | bandit | src/ |
| Dependency vulnerabilities | pip-audit | The locked set, --strict --no-deps |
| Code scanning | CodeQL | security-extended queries |
| Filesystem and config | trivy | HIGH and CRITICAL |
| Container image | trivy | HIGH and CRITICAL, plus a non-root assertion |
A HIGH or CRITICAL dependency finding fails the build. Time-boxed exceptions are
recorded in security/audit-exceptions.md with
an identifier, a reason, a compensating control and a review date — never as a
bare entry in an ignore list.
Container #
The image runs as uid 10001 with no build toolchain in the final layer, and
docker-compose.yml sets network_mode: none on every service except the one
whose purpose is to export. Turning the network on is a visible act in a
reviewed file rather than a default nobody noticed.
Dependencies #
Four at runtime: pydantic, pydantic-settings, structlog, pyyaml. The
OTLP exporter is a hundred lines of stdlib urllib rather than a tracing SDK,
specifically so that the largest dependency in the project is not the one
serving its least-used component. uv.lock is committed and CI installs with
--locked.