Issue #677 Certificate Producer Ownership Preflight¶
This note fixes the architecture guardrails for making Wazuh certificate generation host-user-owned from creation. It is design guidance, not an implementation plan. ADR-007 remains authoritative for the Python lab control plane, ADR-029 for secret handling, ADR-031 for startup results and ordering, ADR-034 for certificate boundaries, ADR-037 for Docker execution, and ADR-043 for the broader rule that a producer must not mutate host-source ownership.
No new ADR is needed. This issue removes an unsafe implementation detail while preserving those accepted boundaries.
Architecture Decisions¶
- Keep
aptl.core.certs.ensure_ssl_certs()as the single owner of legacy Wazuh certificate generation. On native Linux Docker, the generator process must run under the invoking host UID/GID and the bind source must be created by the host user before Compose runs. Generated ownership is a producer invariant, not a cleanup concern. - Reuse
aptl.core.hostenv.needs_host_ownership_fix()as the only gate for native-Linux ownership semantics. Resolveos.getuid()andos.getgid()only inside that true branch. Docker Desktop, other Docker VMs, Windows, an unavailable engine, and an unknown engine must not evaluate either POSIX API or receive a numeric user override. - Remove the post-generation ownership-repair operation completely. A root
helper container is still post-hoc
chown; replacing hostsudowith Docker root does not satisfy the producer-ownership boundary. Do not retain a dormant repair command or fallback to it when user-mode generation fails. - Keep ownership and permission modes distinct.
_ensure_container_readable_certs()remains the canonical host/container readability contract: an owner-only0700directory protects the bundle while0644PEM files remain readable by non-root Wazuh consumers whose container UID may differ from the host UID. A non-privilegedchmodby the owning host user is not ownership repair and must not be replaced withchownor indiscriminately gated off Docker Desktop. - Preserve the isolated Compose project name, generator timeout, unconditional
down --remove-orphanscleanup, manager-root-CA alias, and fail-closedCertResultoutcome. Ownership must not be fixed by rejoining the generator to the lab networks or skipping cleanup of its temporary network. - Preserve the injected
run_commandseam used by typed stateful realization. Do not add a generic Docker passthrough, a second generator service, a new result DTO, or a certificate-specific exception hierarchy.
Compatibility Constraint¶
Repository history matters here. The first host-user implementation was later
replaced because the pinned wazuh/wazuh-certs-generator:0.0.2 image's default
entrypoint did not execute successfully as an arbitrary host UID. The current
root-plus-repair behavior documents that constraint in certs.py.
Consequently, adding --user to the command is necessary but not sufficient.
The generator invocation must prove that the pinned image's real certificate
workflow completes under that identity. If its entrypoint mode requires an
interpreter/entrypoint override, keep that override narrow, fixed to trusted
image-internal paths, and in the same Compose run. It must not bypass the Wazuh
certificate workflow, interpolate project data into shell text, restore root
execution, or add a repair phase. An image incompatibility is a blocking
producer-boundary problem, not permission to fall back silently.
Keep the image reference canonical in generate-indexer-certs.yml. An image
upgrade or locally maintained replacement has supply chain and certificate
compatibility consequences and is outside this issue unless separately
authorized.
The producer fix governs newly generated bundles. A checkout that already has a foreign-owned historical bundle is migration debt: the existing-bundle no-op cannot make it host-manageable without a separate rotation, retirement, or privileged-recovery decision. Do not hide that state behind a successful no-op, silently rotate trusted material, or reintroduce a repair container. If the acceptance contract is intended to cover upgrades with pre-existing root-owned bundles, that migration requires explicit scope before coding.
Cross-Cutting Layers and Canonical Incumbents¶
| Layer | Existing owner and required behavior |
|---|---|
| Host/runtime classification | aptl.core.hostenv.docker_mode() and needs_host_ownership_fix() distinguish native Linux from Docker Desktop/VM/unknown modes. Do not duplicate sys.platform, Docker-info parsing, WSL detection, or OS-name branches in certs.py. |
| Certificate orchestration | aptl.core.certs.ensure_ssl_certs(), _cert_generator_command(), _execute_command(), _cert_generator_project_name(), and _cleanup_cert_generator() own generation, backend-runner injection, project isolation, timeouts, and cleanup. |
| Generator inputs | generate-indexer-certs.yml owns the generator image/service and bind mounts; config/certs.yml owns Wazuh certificate subjects and addresses. Add no PUID/PGID environment convention, duplicate Compose file, or second certificate schema. |
| Startup workflow | aptl.core.lab._step_generate_certs() and _LAB_START_STEPS own ordering and fatal conversion to LabResult. Generation remains before bind-mount validation and container startup. |
| Generated-artifact validation | ComposeStatefulRealizationMixin, stateful_realization_errors(), _canonical_generated_path(), and validate_certificate_bundle() own remote-artifact refusal, output/path shape checks, no-symlink containment, declared-output presence, chain/key/SAN checks, and permission checks. The ownership change must pass these gates rather than duplicate them in the command builder. |
| Bind consumers | docker-compose.yml is the canonical Wazuh manager/indexer/dashboard read-only mount topology. The generator UID does not change the runtime users or mount destinations of those services. |
| Secret storage and packaging | .gitignore, aptl._asset_manifest.EXCLUDED_DIR_NAMES, hatch_build.py, and the asset tests keep generated Wazuh and SOC key material out of Git, wheels, and initialized source bundles. |
| Results and observability | CertResult, LabResult, LabActionResponse, get_logger(), and ADR-029 are the existing error/log surfaces. Failures may name the generation or cleanup layer, but must not include PEM/key bytes, secrets, or a full command line. |
| Tests and workflow | tests/test_certs.py, tests/test_hostenv.py, tests/test_lab.py, tests/test_deployment_stateful_realization.py, tests/test_no_silent_privilege_escalation.py, asset/build-hook tests, pytest, and pre-commit run --all-files are the canonical gates. A clean native-Linux lab boot is the required product proof. |
The SOC certificate generator in aptl.core.soc_ca is deliberately separate:
it generates config/soc_certs/ in-process as the host user and already owns
its registry, atomic writes, containment, key modes, and secret-safe error
envelope. Do not merge the Wazuh and SOC generators or their CertResult
classes as part of this issue. SOC bring-up is a regression surface, not a
second ownership implementation target.
Security and Validation Layers¶
- Authentication surface: this change adds no request or command option.
API-triggered starts remain behind the existing
verify_tokenrouter dependency; CLI starts remain local operator actions. UID/GID must be derived locally, never accepted from an API body, CLI flag,aptl.json,.env, or a scenario. - Environment and config shapes: no environment variable or strict
AptlConfigshape changes.config/certs.ymlremains the sole non-secret certificate provenance input, and typed realization continues to accept only the existingcertificate_bundleartifact/output schema. - Generated-path gate: the typed path must continue through
_canonical_generated_path()before Docker mutation; declared outputs must remain safe relative paths and read-only consumers. The legacy generator must not weaken that no-symlink/project-root rule when pre-creating or changing modes on the output directory. - Certificate validator: declared bundles continue through
validate_certificate_bundle()for file presence, symlink and mode safety, root/manager-root consistency, current validity, key pairing, issuer chain, and SAN/provenance agreement. User ownership alone is not a valid bundle. - Secret handling: private keys remain under the gitignored generated
directory. Preserve the
0700parent and read-only Compose mounts; never log, print, archive, package, or return certificate/key contents. UID/GID are not secrets, but there is no operator value in logging them. - OS/process exposure: pass Docker arguments as a list. The numeric
UID:GIDmay appear only as the non-secret value of Compose's--userargument. Do not invokeid,sudo,chown, a host shell, or a root helper container; do not place PEM, key, credential, or environment values in argv. - Error envelopes: preserve subprocess timeout handling and convert failure
through
CertResultto fatalLabResult/LabActionResponse. Any new compatibility failure should name a stable layer and exit status, not echo a command, host environment, key path inventory, or unredacted upstream body. - Persistence and evidence: no repository/database transaction is involved. Stateful realization may persist only its existing non-secret public-root fingerprint and validation booleans; ownership metadata and private material do not belong in run records or RAES evidence.
Extensibility Seam¶
The seam is the existing host-environment decision plus the optional execution
identity consumed by the certificate command builder, with Docker execution
still injectable through run_command. The next reasonable host-runtime
variation should change hostenv classification or the identity returned by
that boundary once, not add another OS branch or ownership strategy to
certificate orchestration.
The generator image/service/version remains parameterized by the canonical Compose asset. A future generator-image change should not require copying its reference into Python, deployment DTOs, docs, or a repair command.
Whole-Repository Surface¶
- Certificate producer and platform boundary:
src/aptl/core/certs.pyandsrc/aptl/core/hostenv.py. - Startup and deployment boundary:
src/aptl/core/lab.pyandsrc/aptl/core/deployment/_compose_stateful_{constants,graph,realization}.py. - Certificate validation:
src/aptl/core/deployment/_stateful_certificates.py. - Canonical assets and consumers:
generate-indexer-certs.yml,config/certs.yml, and the Wazuh mounts indocker-compose.yml. - Adjacent SOC regression boundary:
src/aptl/core/soc_ca.py,src/aptl/core/_soc_ca_io.py, and theconfig/soc_certs/mounts. - Distribution and secret exclusion:
.gitignore,src/aptl/_asset_manifest.py,hatch_build.py, and their tests. - Operator guidance:
docs/getting-started/installation.md,docs/deployment.md, anddocs/troubleshooting/index.md. Manual raw Compose certificate commands bypass the platform-aware identity policy and must not remain the recommended regeneration path. - Verification: certificate, host-environment, startup, stateful-realization, privilege-escalation, asset, and package-build tests plus native Linux and Docker Desktop lab-start evidence.
Gotchas and Anti-Patterns¶
- Do not conflate host file owner, generator process user, and the fixed
non-root UIDs used later by Wazuh services.
--user <host UID>:<host GID>applies only to the one-shot producer. - Do not conflate UID/GID ownership with
0700/0644mode policy. Removing the readability pass can make correctly owned certs unusable by Wazuh. - Pre-create the native-Linux bind source as the host user. Otherwise Docker
may create the missing host directory as root before container
--usertakes effect. - Do not call
os.getuid()/os.getgid()before the platform gate, including while computing defaults, formatting log arguments, or constructing test fixtures. Those attributes are absent on native Windows Python. - Do not use a mocked Docker Desktop unit path as proof of host ownership. The file-sharing result is a runtime contract and needs live Desktop evidence; the unit test proves only command construction and absent POSIX calls.
- Do not treat Linux host, Linux Docker Desktop, WSL2, Colima/Lima, and native Linux Docker as interchangeable. The repository's Docker-mode probe is the policy boundary.
- Do not restore host
sudo, Docker-rootchown, a root entrypoint phase, or a best-effort fallback. Generation failure must stop startup. - Do not remove isolated-project cleanup while changing command ordering; a leaked generator network can overlap the TechVault networks.
- Do not update only the legacy
_step_generate_certs()path. Typed RAES realization calls the same generator with an injected backend runner and then applies stronger output validation. - Do not leave documentation telling operators to regenerate with raw
docker compose -f generate-indexer-certs.yml run ...; that bypasses the platform policy and can recreate the root-owned state. - Do not treat the repository-wide
sudo -nAST guard as the complete acceptance test. The certificate path must separately prove that it issues nosudo,chown, root helper, or repair command at all. - Do not broaden this issue into Wazuh certificate rotation, CA redesign, service UID remapping, generalized host-file repair, or deployment-backend redesign.
Non-Goals and Boundaries¶
This issue does not change certificate subjects/SANs, Wazuh TLS topology, private-key distribution, Wazuh service runtime users, SOC CA generation, Compose profiles, remote generated-artifact support, RAES schemas, durable configuration, environment variables, API/web contracts, startup readiness taxonomy, run-record schemas, or certificate rotation. It does not upgrade the generator image, introduce a custom image, or add a generic ownership manager without separate authorization and supply chain review.