Settle the TLS resolution ladder: the unanchored default, the system fallback, and the explicit-none latch
Component: client-agent (config.c), https_client (moduleConfig.cpp)
Depends on: the MVP branch feat/39021-poc-agent-https-tier2
Rough size: M
Description
What an agent does when it has nothing to verify with — and what it does when it has something but is told not to use it. Both are settled in w_agent_resolve_ssl_posture() (src/client-agent/src/config.c), and on the MVP branch both differ from the design.
This issue was originally scoped to a single system → full fallback. Commit 24d9dd7b3a ("simplify verification mode none scenarios") changed the ground under that: the ladder's last rung is no longer system, so there is no longer a system default to fall back from. The scope is now the rung itself, the fallback, and the latch — they are the same function and cannot be decided apart.
The ladder as it stands:
explicit <verification_mode> -> honoured, including 'none' with an anchor present
explicit <certificate_authorities>, no mode -> certificate
anchor file present, no mode -> full (the anchor is injected as the CA)
nothing at all -> none <- design says system1. The last rung: none or system
An agent with no CA, no anchor and no explicit mode connects unverified. That is the defect #38940 exists to remove, reachable on a stock install. Branch 5.0.0 resolves that case to system.
The reason for the change is real, and it is why this is not a one-line revert: system cannot validate a self-signed remoted.pem, so resolving there makes a stock install against a private-CA manager fail to connect rather than connect unverified. Trading a silent downgrade for a dead agent is not obviously the better default, and 24d9dd7b3a chose the connection.
The designed answer to that trade is the fallback below — system first, the local anchor when the OS store cannot do the job, and a hard failure only when neither can. That is what makes system affordable as the last rung, and it is why the two halves have to be decided together.
- Decide the last rung, with the fallback in hand rather than before it.
- Whichever is chosen, make the
<ssl>block in the packagedossec.confand the WPK gates' log text say the same thing — both gates currently hard-code the branch's answer.
2. The system → local-anchor fallback
When the OS trust store cannot verify the manager, resolve to full against a certificate under etc/certs and log a warning; when neither works, log critical and fail closed.
- Decide what "cannot verify" means.
os_find_ca_bundle()is astat()over five hardcoded paths — it fires only on a host with no CA bundle at all, not on the case operators hit, which is a bundle that exists and has never heard of the deployment's CA. Detecting the latter needs a live verified probe or treating the first handshake failure as the trigger.pkg_installer.sh'sprobe_server_verified()already does exactly that for WPK upgrades and pinsDEFAULT_CA_FILEon failure — same policy, same path, already in the tree. - Make the three enforcement points agree:
w_agent_resolve_ssl_posture(),w_agent_validate_ssl_ca()andmoduleConfig.cpp'svalidateTls(). A fallback in one and not the others gives a daemon that passes the startup gate and dies at module init. - Decide what happens to
(4120)—systemwith an explicitcertificate_authoritiesis a hard refusal today. If a local certificate is now a legitimate fallback undersystem, an operator who configured one is arguably asking for it. - Windows and macOS cannot introspect their native stores at validation time, so trigger (a) does not exist there. State what each platform does rather than leaving it implicit.
The reverse (full → system) — proposed, and not recommended
The two directions are not symmetric. system → full narrows trust to one pinned, deployment-specific CA. full → system widens it to every CA in the OS store, including any traffic-inspection CA, so a certificate the pin correctly refused would succeed instead. It also contradicts §2.3's requirement that the fetched CA be the sole anchor, "not an addition to the system store, not one candidate among several", and the benign case is already covered — no anchor at all resolves down the ladder anyway, so the reverse only ever fires when someone asked for pinning and it failed.
- If it is built anyway: missing-file case only, at startup, never in response to a verification or handshake failure, logged at error.
3. The latch: does an explicit none survive a present anchor?
The branch honours it, warning (4122) with text naming the anchor and the line to remove. Item 33, §2.7 limit 3 and §3.1 say the opposite — keep verifying, log the ignored downgrade at error, never refuse to start.
Both are defensible. For honouring: an explicit setting is an instruction, and the warning is not silent. For the design: "explicitly written" is a poor proxy for "deliberately chosen" here, because §4.1 records that every current Ansible role and Puppet module ships verification_mode=none as its default. The concrete failure is an agent that bootstraps with a token, pins an anchor, enrols verified — and then a configuration-management converge re-templates ossec.conf with the role's default, after which every request goes unverified against a manager it could verify. The design's escape hatch is deliberately the anchor file rather than ossec.conf, because that is the file automation does not rewrite.
So the question is which file is the off switch, not whether explicit means explicit.
- Decide, and document the off switch either way.
- Either way, remove the dead branch. The upgrade-to-
fullpath is guarded by!verification_mode_explicit, and that flag is set whereverRead_Agent_SSL()parses the tag (client-config.c:1099), while an unparsed mode arrives asAGENT_VERIFY_UNSETand is consumed by the first rung — so nothing in production reaches it. It is exercised only by a unit test that sets the flag by hand. A latch that cannot fire should not look like one.
Definition of Done
- A stock install with no token, no CA and no anchor lands on the decided posture, and the packaged configuration, both WPK gates and the documentation all state the same one.
- An agent set to
systemon a host whose OS store cannot verify the manager, with a valid local certificate, connects verified and logs one warning saying it fell back. - The same agent with no usable local certificate logs critical and does not start — no silent downgrade, no daemon left running with a dead transport.
- An agent whose OS store does verify behaves exactly as today, with no warning and no fallback.
- The latch behaves as decided, with a test for the token-install-then-converge sequence specifically.
- No unreachable branch is left behind in the resolver.
- The startup gate and the module agree on every combination.
Related: #38940, #39021
Source: wazuh/wazuh