ADR-043: Suricata Runtime Config Ownership Boundary¶
Status¶
accepted
Date¶
2026-06-20
Context¶
Issue #325 closes an ownership regression in the Suricata lab service. The
upstream jasonish/suricata:7.0 image entrypoint chowns Suricata config and
rule paths to the image's suricata user. In the image that UID is 991; on
Ubuntu hosts the same numeric UID commonly belongs to systemd-network. When
Suricata sees host bind mounts at those paths, the in-container chown rewrites
host-side ownership. The affected paths are:
- checked-in source config:
config/suricata/suricata.yamlconfig/suricata/rules/local.rules- generated runtime rules:
.aptl/suricata/rules/misp/
Making the checked-in binds read-only is not a valid fix. The upstream
entrypoint runs under set -e; a failed chown exits the container and leaves
Suricata crash-looping.
Existing decisions constrain the repair:
- ADR-019 keeps Suricata IDS-only. This issue must not alter detection versus prevention semantics.
- ADR-022 owns MISP-driven Suricata rule semantics: alert-only translation, stable SIDs, hash sidecar files, idempotent writes, and unix-command reload.
- ADR-028 owns the source-owned versus generated-artifact boundary. Checked-in
config/files are inputs, not runtime mutation targets. - ADR-031 owns lab-start orchestration: a flat sequence of
_step_*functions returningLabResult | None, with narrow secret-safe contract failures. - ADR-037 owns Docker access through
DeploymentBackend; new Docker behavior must not become a generic argv passthrough.
This ADR supersedes only the Suricata concrete realization in ADR-028 and the host-bind wording in ADR-022. Their rule semantics and source-owned configuration boundary remain authoritative.
Decision¶
Choose the named-volume plus entrypoint-wrapper design.
Suricata must no longer bind-mount checked-in config/suricata/... files, or
the generated .aptl/suricata/rules/misp/ tree, onto paths the upstream
entrypoint chowns.
At lab start, APTL materializes Suricata runtime inputs into Docker project-scoped named volumes:
- a config seed volume sourced from
config/suricata/; - a MISP rule volume seeded from
config/suricata/rules/misp/and shared read-write bysuricataandmisp-suricata-sync.
The Suricata service mounts the config seed volume at a non-upstream path and
uses a small wrapper entrypoint. The wrapper copies the seeded config into the
image-owned /etc/suricata/ tree, then execs the upstream image entrypoint
with the original command arguments. The upstream entrypoint may still chown
/etc/suricata/... and /var/lib/suricata/rules/misp, but those are now the
container writable layer and Docker-managed named volumes, not host source
files.
The internal Suricata paths stay stable:
/etc/suricata/suricata.yaml/etc/suricata/rules/local.rulesdefault-rule-path: /var/lib/suricata/rulesmisp/misp-iocs.rulesplusmisp-*.listsidecars
The MISP sync service keeps writing RULES_OUT_PATH under
/var/lib/suricata/rules/misp/ and keeps using the existing unix-command
socket reload path. Do not merge operator-authored local.rules and
MISP-generated rules.
The implementation must also retire the legacy host bind directory
.aptl/suricata/rules/misp/. Prior lab runs may already have left it owned by
UID 991, so lab start may need a narrow Docker-root cleanup or repair for that
legacy path. That cleanup is compatibility debt only: after this ADR, the path
is not a Suricata runtime source. It must be path-contained, reject symlinked
chains, and target only the canonical legacy project-relative path.
Do not set explicit global Docker volume names. Compose project scoping must
continue to derive names from the configured compose project, so multiple
worktrees and custom deployment.project_name values do not collide.
Rejected Options¶
Image-side copy via command¶
Rejected. Compose command: is passed to the upstream entrypoint; it does not
run before the entrypoint's chown. Using command: either leaves the chown trap
in place or bypasses the upstream entrypoint entirely, which creates a second
Suricata startup contract.
Rendered .aptl/suricata/config/ bind mount plus recurring root cleanup¶
Rejected as the primary design. It keeps the host bind mount under a path the Suricata image chowns, then relies on a privileged cleanup before every subsequent start. That repairs the symptom but preserves the host ownership hazard and makes idempotency depend on Docker-root deletion of host paths.
Narrow legacy cleanup is allowed only to recover from pre-ADR-043 .aptl/
ownership damage and to make that retired path manageable again.
Guardrails¶
- Keep source config, runtime seed data, Docker named volumes, and persisted service state as separate concepts.
- Keep Suricata startup under
aptl lab start; a baredocker compose upagainst a fresh checkout is not required to materialize seed volumes. - Place any seed orchestration in the existing lab-start sequence before
_step_check_bind_mountsand_step_start_containers; seed failures are fatalLabResultfailures because Suricata would otherwise start with stale or absent config. - Reuse or extract the existing containment and no-symlink-chain checks from
src/aptl/core/credentials.py; do not add weaker path checks. - Reuse
PathContainmentError,CredentialRenderErrorwhere applicable,LabResult, and the existing redacted logging/diagnostic surfaces. Do not add a Suricata-specific exception hierarchy or result DTO. - Route Docker behavior through a narrow backend-owned operation if code must
invoke a seed container. Do not add
docker(args),host_run(args), or any generic passthrough toDeploymentBackend. - Preserve argv-list subprocess construction. Do not concatenate project paths, volume names, service names, or shell fragments into unvalidated shell strings.
- Preserve
misp-suricata-sync's existingServiceConfig,rule_writer, IOC validation, alert-only translator, and reload retry behavior. - If new durable knobs are needed, add them to strict
AptlConfig. Do not introduce ad hoc environment variables or a second config schema for Suricata seeding. - Keep the wrapper script data-free. It may name fixed container paths, but it
must not carry
.envvalues, API keys, certificates, or operator secrets. - Regression coverage must pin first-run seeding, repeated-start idempotency,
a prior UID-991-owned legacy
.aptl/suricata/rules/misp/tree, and unchanged host ownership for the checked-in Suricata source files.
Security Layers¶
- Environment binding: this design adds no new secret-bearing environment
values. Existing MISP and Wazuh secrets still flow through
load_dotenv,EnvVars,find_placeholder_env_values, and the MISP sync service'sServiceConfig.from_env(). - First-party config shape: durable operator controls stay in
AptlConfigwithextra="forbid". The default path should need no new operator config. - Filesystem containment: source reads are limited to canonical
project-relative paths under
config/suricata/. Legacy.aptlcleanup is limited to the canonical retired path and must reject symlinked path components before Docker-root cleanup runs. - Docker boundary: named volumes are Compose project-scoped and reached through the deployment backend's runner semantics. Shared daemon behavior must preserve ADR-037 project scoping.
- Container runtime boundary: Suricata's upstream entrypoint remains the authority for the image's normal startup. The wrapper only stages config into the image-owned path before delegating.
- OS/process exposure: seed operations and wrapper invocation must pass non-secret fixed paths and volume names via argv-list commands. No secrets may appear in process argv, Compose command strings, wrapper text, or logs.
- Error envelopes: failures return existing
LabResulterrors naming the artifact or validation layer. Raw Docker stderr and exception text must be redacted before crossing logs, CLI, API, web, telemetry, or run archives. - Serialization boundaries: snapshots, status output, and exports must not add generated config or named-volume contents as unredacted artifacts.
Maintainability¶
Canonical incumbents the implementation must build on:
src/aptl/core/lab.pyfor lab-start sequencing and fatal/non-fatal result behavior.src/aptl/core/credentials.pyfor path containment, generated artifact errors, mode enforcement precedent, and Suricata baseline constants.src/aptl/core/deployment/for Docker Compose execution and project scoping.docker-compose.ymlfor Suricata,misp-suricata-sync, and named-volume wiring.src/aptl/services/misp_suricata_sync/for IOC translation, idempotent rule writes, and reload behavior.tests/test_lab.pyandtests/test_credentials.pyfor orchestration and seed/ownership regression coverage.
Do not duplicate the MISP sync schema, the Suricata rule writer, the lab result envelope, the Docker backend runner, or the config/env validators.
Extensibility¶
The seam is a typed named-volume seed specification:
- canonical project-relative source path;
- Compose project-scoped volume name;
- destination subpath inside the volume;
- whether the volume is seed-only or shared read-write at runtime;
- optional canonical legacy host path to repair or retire.
One future generated-volume service should be addable by registering another seed spec and reusing the same backend-owned materialization path, not by adding another one-off Docker cleanup or another generated-artifact schema.
Whole-Repo Surface¶
This decision touches:
docker-compose.ymlservice mounts, entrypoint, and top-level volumes;src/aptl/core/lab.pystartup ordering and failure envelopes;src/aptl/core/credentials.pyor a small extracted helper for canonical Suricata seed paths and containment;src/aptl/core/deployment/if seed containers require backend support;containers/misp-suricata-sync/Dockerfilecomments that currently describe a host bind mount;config/suricata/suricata.yamlpath invariants;tests/test_lab.py,tests/test_credentials.py, and compose/inventory assertions that inspect Suricata mounts.
Non-Goals¶
- Do not build or maintain a custom Suricata image.
- Do not upgrade or replace
jasonish/suricata:7.0as part of this issue. - Do not redesign Suricata detection, MISP IOC semantics, Wazuh active response, or SOC seeding.
- Do not change Wazuh credential rendering or the broader ADR-028 generated config model.
- Do not solve general remote generated-artifact materialization unless the deployment backend implements it explicitly. Refusing SSH-remote startup with the existing ADR-028 shape remains acceptable.
Anti-Patterns¶
- Read-only binding checked-in Suricata files directly into
/etc/suricata. - Binding
.aptl/suricata/...to a path the Suricata image chowns and relying on recurring root cleanup as the steady-state design. - Using
command:to try to run setup before the upstream entrypoint. - Adding broad
rm -rfhelpers, shell-string Docker commands, or generic Docker passthroughs. - Setting explicit unscoped Docker volume names.
- Rewriting
suricata.yamlinternal paths to match a host workaround. - Merging
local.ruleswith MISP-generated rules. - Duplicating env/config validation, exception hierarchies, logging policy, or MISP rule-writing logic for this issue.