GitHub
08/09/2026, 9:09 PMServiceParameters and used it directly — DB edits had no runtime effect. An operator could change debug.enableHttp to true in the frontend, but the service kept using the YAML value on the next boot.
Changes
pkg/serviceconfig — Resolve method
Resolve(service, cfg, envID) reads all sections for a service from the DB and, for any section with source=db, unmarshals the JSON value into the matching ServiceParameters field — overriding the YAML default. Sections with source=yaml are skipped (the YAML value is already in ServiceParameters from the initial load).
The applySection helper maps each section name to its ServiceParameters field and does the json.Unmarshal. All 15 sections are handled (service, db, redis, osquery, tls, logger, carver, debug, batchWriter, configEndpoints, osctrld, metrics, saml, oidc, jwt).
Startup wiring
Both services now call Resolve right after `Seed`:
• `cmd/tls/main.go`: Seed → Resolve — DB-edited sections override YAML defaults in flagParams before any downstream consumer reads them.
• `cmd/api/main.go`: same flow for the API service.
The startup flow is now: load YAML → seed to DB (create-if-missing) → resolve DB overrides back into ServiceParameters. Every downstream consumer of flagParams (logger, carver, metrics, TLS, handlers, etc.) gets the resolved values.
Tests (8 new in pkg/serviceconfig/serviceconfig_test.go)
• TestResolve_NoDBEdits_KeepsYAMLValues — no-op when nothing is DB-edited.
• TestResolve_DBEditedSection_OverridesYAML — debug section override.
• TestResolve_ServiceSection_OverridesYAML — service section override (listener, port, host, log level, audit log).
• TestResolve_YAMLSourceSectionsAreSkipped — source=yaml sections are not touched.
• TestResolve_UnknownService — rejects unknown service.
• TestResolve_NilConfig — rejects nil config.
• TestResolve_FullRoundTrip — seed → edit → re-seed → resolve: DB value wins over fresh YAML.
• TestResolve_MultipleDBEdits — multiple sections resolved simultaneously.
Validation
• go build ./... — clean
• go test ./... — all packages pass (8 new tests in pkg/serviceconfig)
• golangci-lint run ./pkg/serviceconfig/... — 0 issues
• gofmt clean on all modified files
How it works end-to-end
1. First boot: YAML loads into ServiceParameters → Seed creates DB rows with source=yaml → Resolve finds nothing to override (all source=yaml) → service uses YAML values.
2. Operator edits debug in the frontend: UpdateSection flips source=db and stores the new JSON.
3. Next boot: YAML loads into ServiceParameters → Seed doesn't clobber the source=db row (create-if-missing) → Resolve sees source=db, unmarshals the DB value into cfg.Debug → service uses the DB-edited value.
Security notes
• Resolve applies all sections with source=db, including non-editable ones like db and redis. This is intentional: non-editable sections can only get source=db through direct DB access (operator/automation), not through the API. The Editable flag gates the API write path; Resolve trusts any row the operator put in the DB.
• The YAML file remains the bootstrap — if the DB is empty or the table is dropped, the service falls back to pure YAML behavior.
jmpsec/osctrlGitHub
08/09/2026, 9:14 PM