Skip to content

Architecture ADRs: the client's future shape, runtime, identity, and API - #149

Open
darksidemilk wants to merge 5 commits into
masterfrom
claude/fog-client-last-commit-d6x547
Open

Architecture ADRs: the client's future shape, runtime, identity, and API#149
darksidemilk wants to merge 5 commits into
masterfrom
claude/fog-client-last-commit-d6x547

Conversation

@darksidemilk

@darksidemilk darksidemilk commented Sep 3, 2026

Copy link
Copy Markdown
Member

Exploratory. Nothing is implemented and no code is changed — four Markdown files under docs/adr/, nothing else. One of the four deliberately carries no recommendation; it is written to be settled here, in review.

fog-client has had no functional commit since March 2023 (v0.13.0) while the server it talks to shipped PKI work this year and still pins FOG_CLIENT_VERSION at 0.13.0. These documents record what turned up when we went looking at why, and what the options actually are.

The number the exploration turned on

find Modules -name '*.cs' -not -name '*.Designer.cs' | xargs cat | wc -l          # 3013
find UniversalInstaller SetupHelper UpdateHelper UpdateWaiter \
     ../zazzles/Zazzles/Modules/Updater ../zazzles/Zazzles/Bus \
     -name '*.cs' | xargs cat | wc -l                                            # 5658

Nearly twice as much code exists to let the agent be a self-updating compiled binary with its own IPC bus as exists to do the work. Of 17,526 lines across this repo and zazzles, roughly 3,000 are the product. Every other observation is downstream of that ratio.

The four ADRs

0001 — How much of FOG belongs on the endpointopen, no recommendation. Deploy-time provisioning plus signed scripts on native schedulers, against keeping a compiled agent, with a module-by-module feasibility table. It does settle two things: agentless push is rejected in both futures (the server would have to store reusable fleet credentials on arguably the worst-defended box on the network; it cannot route to roaming or NATed clients; and on non-domain-joined Windows both WinRM and schtasks /s need a GPO or an agent to enable the thing meant to replace the agent), and a "thin agent" is a ratchet rather than a compromise — because it is compiled it needs an updater, which needs a second process, which needs a bus. That is not speculation; it is the 5,658 lines above.

Worth knowing before reading: the rename module is already agentless in production. funcs.sh:changeHostname() mounts the deployed NTFS volume and rewrites twenty registry values through reged. The C# path is a fallback.

0002 — The runtime, if a compiled agent survivesconditional on 0001. Recommends the current .NET LTS. Only five language constructs actually block the port (Thread.Abort, AppDomain.CreateDomain, the RSACryptoServiceProvider casts, AesCryptoServiceProvider, ServiceBase.Run), and the per-OS seam the Windows-only packages need already existsSystem.Management being Windows-only costs nothing when its callers are already in Windows/ directories. Rejects Go (not worse, but worse from here), Rust (maintainer pool, said plainly), Node (posture for a SYSTEM service), Electron (a service has no window).

0003 — Client identity belongs in the application layer, not the certificate chainindependent of 0001. A per-client Ed25519 keypair plus ordinary platform TLS, over the per-client X.509/mTLS that "issued certs per client" suggests — that would reintroduce an online CA key months after GHSA-94p8-jg9j-99v4 caused one to be taken offline, and its revocation story degrades to a DB allowlist anyway. Covers migration with no machine touches, and the imaging problem at length.

Its Wall 1 was rewritten after finding fogproject ADR-0039 (2026-09-02), which landed SmbiosIdentity and deliberately kept the MAC as the identity. 0039 solved resolution; this is about authentication, which 0039 left untouched.

0004 — The client API is described in FOG's own OpenAPI documentnew, independent of all three. Prompted by FOGProject/fog-sdk, created 2026-08-29, which generates PowerShell and Python clients from a pinned snapshot of FOG's live OpenAPI document.

The SDK cannot go on endpoints, and the reason is the useful part: all 380 paths in the snapshot are the administrative CRUD surface — there is no host check-in surface in the document at all — and all three security schemes authenticate a person, with tokens that act with their owner's roles, carry no scope, never expire, and are revocable only in the UI. Shipping one to a thousand workstations is the exact inverse of 0003.

So the SDK isn't the answer; the pipeline behind it is. Define the client protocol as real REST operations in the same document, under a separate hostAuth scheme, and generate the endpoint's HTTP layer instead of hand-writing it. The strongest support came from this project's own tests/openapi-route-coverage.test.php, whose docblock makes the argument unprompted: "a client generated from the document simply has no method for the missing endpoint, and the reasonable conclusion is that the server cannot do it." The host protocol is the largest body of routes entirely outside that guarantee.

Three claims about this codebase that are widely repeated and wrong

Each has been used to argue for a conclusion it does not support:

  • AD join is not System.DirectoryServices — it is a netapi32 P/Invoke (NetJoinDomain/NetUnjoinDomain). Any runtime with FFI calls it identically, so this is not an argument for .NET.
  • There is no WMI inventory in the client (that is FOS's job). The whole surface is four queries.
  • Printer management is mostly shell-outs (rundll32 printui.dll, lpadmin), which lowers the bar for a non-.NET runtime.

And one about the docs: a Let's Encrypt certificate on the vhost does not break fog-client. Both copies of that page are internally inconsistent rather than wrong — the How FOG uses certificates section opens with a callout stating the correct post-zone-split position, while the body and TL;DR still name fog-client as the constraint and recommend step-ca.

A note on scope

0003 mentions a live vulnerability found while writing it and deliberately does not describe it, because this PR is public and the fix is small enough that the diff would disclose it. It's written up separately for the private advisory process in fogproject's SECURITY.md. Those fixes should not wait for any decision here.

Review

The useful thing to push back on is 0001 — that is the point of it being open. The weakest quantitative claim is 0002's 60–90 MB self-contained payload estimate, flagged in the text as an estimate. The LOC figures reproduce with the commands above.

None of these propose a change to fogproject or fog-sdk; references to those repos' ADRs and tests are reading references only.

🤖 Generated with Claude Code

https://claude.ai/code/session_016hejbUjsNJ7PyMdh8Cpet7

…ntity

fog-client has had no functional commit since March 2023 while the server
it talks to has kept moving. These three ADRs record an exploration of what
to do about that. Nothing is implemented; ADR-0001 is deliberately left open
for the maintainers to settle.

The number the exploration turned on: 3,013 lines under Modules/ do the
actual work, while 5,658 lines across UniversalInstaller, SetupHelper,
UpdateHelper, UpdateWaiter, Updater and Bus exist only to ship those
features as a self-updating compiled binary with its own IPC bus. Both
figures are reproducible with the commands quoted in ADR-0001.

0001 — How much of FOG belongs on the endpoint. Open, no recommendation.
  Lays out deploy-time-plus-signed-scripts against keeping a compiled agent.
  Does settle two things: agentless push is rejected in both futures (the
  server would have to store reusable fleet credentials, it cannot route to
  roaming clients, and WinRM/schtasks need a GPO or an agent to enable the
  thing meant to replace the agent), and a "thin agent" is a ratchet rather
  than a compromise. Notes that the rename module is already agentless in
  production via FOS's changeHostname().

0002 — The runtime, if a compiled agent survives. Conditional on 0001.
  Recommends the current .NET LTS: only five language constructs actually
  block the port, and the per-OS seam the platform packages need already
  exists. Rejects Go (a rewrite from here), Rust (maintainer pool), Node
  (posture for a SYSTEM service) and Electron (a service has no window).
  States the payload regression as an estimate to be measured, not a fact.

0003 — Client identity belongs in the application layer, not the
  certificate chain. Independent of 0001, so it can be settled first.
  Recommends a per-client Ed25519 keypair plus ordinary platform TLS over
  per-client X.509/mTLS, which would reintroduce the online CA key that
  GHSA-94p8-jg9j-99v4 caused to be taken offline. Covers migration with no
  machine touches, and the imaging problem at length — a credential on disk
  gets cloned by the product's own core function.

Also corrects three claims about this codebase that are widely repeated and
wrong: AD join is a netapi32 P/Invoke rather than System.DirectoryServices,
there is no WMI inventory in the client, and printer management is mostly
shell-outs. Each has been used to argue for a conclusion it does not support.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hejbUjsNJ7PyMdh8Cpet7
darksidemilk and others added 4 commits September 3, 2026 00:45
The first version wrote Wall 1 from the 1.5 line, where a commented-out
sysuuid path stood as evidence that hardware identifiers had been tried and
abandoned. That is no longer the state of the branch this ADR targets.

fogproject ADR-0039 (dated 2026-09-02) landed SmbiosIdentity on working-1.6:
four SMBIOS fields scored in order, a placeholder table from hardware seen in
the wild, and a repeated-character rule that handles the all-F MSI UUID which
sank the 2018 attempt. So the "duplicate UUIDs make this unusable" objection
does not hold here and this ADR should not repeat it.

The wall stands for a different and better reason: 0039 deliberately scoped
itself to the boot path, keeping the MAC as the identity, and says outright
that the FOG client needs no change. It solved resolution -- which host is
this. It did not touch authentication, and cannot, because every value
SmbiosIdentity reads is a string the caller supplies. Identification is not
authentication.

That also makes the imaging section's binding defence cheaper without
reopening what 0039 settled: the signature decides, and the firmware
fingerprint is only a consistency check on a credential that already proved
itself. SmbiosIdentity is reused rather than duplicated because it already
encodes which firmware values are worthless.

Other changes:
- Status now says working-1.6 only, and that the 1.5 line is out of scope
  rather than merely unmentioned. Server paths are 1.6 paths
  (packages/web/src/, not lib/fog/*.class.php).
- References fogproject ADR-0043 (the server half), 0039, 0027 (the same
  plaintext-credential argument one layer up) and 0036/0037/0040 (the PKI
  direction that rejecting per-client X.509 keeps faith with).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hejbUjsNJ7PyMdh8Cpet7
Same correction as fogproject ADR 0043. The page does not simply assert the
stale claim -- its "How FOG uses certificates" section opens with a callout
that already states the correct position, that the zones are independent and
the web certificate can be replaced without disturbing fog-client. What is
stale is the body under that callout and the TL;DR at the top, which still
name fog-client as the constraint and recommend step-ca.

An internal contradiction is a different defect from a wrong page, and the
ADR should describe the one that is actually there. Also names both copies
explicitly (fogproject's docs/EXTERNAL_CA_AND_LETSENCRYPT.md and fog-docs'
docs/kb/integrations/external-ca-lets-encrypt.md) since they carry the same
text and both need the same fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hejbUjsNJ7PyMdh8Cpet7
ADR-0003 linked a companion record proposed in fogproject as ADR-0043. That
proposal has been withdrawn and its PR closed -- the ask here was for ADRs on
fog-client, and opening a numbered record in another repository's curated ADR
sequence went beyond it.

The link is removed rather than repointed, because there is nothing to point
at. Nothing else changes: the server-side consequences this ADR describes are
part of the client-identity proposal and belong in it, and the remaining
fogproject references (0027, 0036, 0037, 0039, 0040) are reading references to
records that already exist on working-1.6.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hejbUjsNJ7PyMdh8Cpet7
FOGProject/fog-sdk was created on 2026-08-29, after the exploration behind
0001-0003, and it changes the arithmetic on a question those three left as
hand-written work: how the endpoint talks to the server. FOG 1.6 already
serves its own OpenAPI document live from /system/openapi, and fog-sdk
already generates PowerShell and Python clients from a pinned snapshot of it.
The spec, the generator and the release process all exist.

The SDK itself cannot go on endpoints, and the reason is worth recording
because "we have an SDK now, use it" is the obvious wrong next step. All 380
paths in the snapshot are the administrative CRUD surface; there is no host
check-in surface in the document at all. And all three security schemes
authenticate a person -- tokens that act with their owner's roles, carry no
scope, never expire, and are revocable only in the UI. Shipping one to a
thousand workstations is the exact inverse of what 0003 argues for.

So the SDK is not the answer; the pipeline behind it is. The decision is to
define the client protocol as real REST operations inside the same document,
under a separate hostAuth security scheme, and generate the endpoint's HTTP
layer rather than hand-write it. Whatever 0003 settles on defines that scheme.

The strongest support came from fogproject's own
tests/openapi-route-coverage.test.php, which already enforces both directions
of route/document agreement and whose docblock makes this ADR's argument
unprompted: a client generated from the document simply has no method for a
missing endpoint, and concludes the server cannot do it. The host protocol is
the largest body of routes entirely outside that guarantee, so bringing it in
puts it under a test that already runs.

Independent of 0001 and 0002 -- a generated client is equally useful to a
compiled agent and to a signed script -- and a companion to 0003 rather than a
dependency of it. Cross-referenced from all three; 0001's status line and
0002's Phase 4 updated to match.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016hejbUjsNJ7PyMdh8Cpet7
@darksidemilk darksidemilk changed the title Architecture ADRs: the client's future shape, runtime, and identity Architecture ADRs: the client's future shape, runtime, identity, and API Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant