<#1154 osctrl-tls: answer unknown environments lik...
# osctrl
g
#1154 osctrl-tls: answer unknown environments like bad credentials Pull request opened by javuto osctrl-tls: answer unknown environments like bad credentials Problem Every osctrl-tls endpoint answered an unknown environment UUID in the URL with 500 and an error log. A real environment with bad credentials got a different answer, such as
200 {"node_invalid": true}
on config, log, query read and query write. Two problems followed: • The difference told a scanner which environment UUIDs exist. • Every scanner request showed up as a server error in logs and 500 metrics. Returning 404 instead would have kept the oracle, so this change removes the difference altogether. What changes • A
getEnv
helper separates "not found" from real lookup errors. Every environment lookup in osctrl-tls then has an explicit unknown-environment branch that returns the same response a known environment gives the same request with bad credentials: • config, log, query read, query write and carves:
200 {"node_invalid": true}
(carve endpoints: their invalid-session response); • enroll: 403; • flags, cert and verify, plus the osctrld status, osquery, extensions and extension-download endpoints: the wrong-secret 403. An unknown environment draws on the same failed-secret budget, so the 429 is the same too; • quick enroll/remove, script and enroll package: their invalid-request responses. • Ordering is kept: a bad request (oversized body 413, malformed JSON, invalid script or package arguments) is rejected the same way for both, before the credential answer. • Nothing falls through with an empty environment. Secret checks, node lookups, node creation and auto-tagging run only for known environments, so an empty enroll secret can never match an attacker's empty secret. • Logging and metrics: • Unknown environments are logged at debug level and counted in
osctrl_tls_unknown_environment_total{handler}
. The label is a fixed handler name, never the UUID. • An unknown-environment enroll writes no audit row, so scanners can't flood
audit_logs
. • Real lookup errors (database down, cancelled context) still return 500 with an error log. • Enroll with enrolls disabled: this used to return 503 before the secret check, so it also revealed the environment. Now only the correct secret gets 503. Security and operational impact • Responses no longer reveal which environment UUIDs exist, and scanner traffic stops showing up as 500s. • Behavior changes for known environments: • Enrolls disabled: a wrong secret now gets 403 and a FailedEnroll audit row (previously 503, with no row). A malformed body gets 413/500 instead of 503. • Config endpoint: a real environment lookup error now returns 500 instead of an empty 200. • What can still tell them apart: • Timing: an unknown UUID always costs a database query, because misses are not cached, while a known environment is usually served from cache. These endpoints take the environment's random v4 UUID, so this only confirms a UUID the attacker already has. • Database outages: a cached known environment still answers while an unknown one gets 500. • Upper-case UUIDs: a known environment's UUID sent in upper case is treated as unknown, which is consistent and leaks nothing. It still appears under the real environment name in the 429 audit row. • No schema or API changes. osctrl-api is not touched. Validation • New `cmd/tls/handlers/unknown_env_test.go`: • about 50 table cases comparing status, Content-Type and full body between a known environment with bad credentials and a random unknown UUID, across every endpoint; • empty-secret cases, which would catch a fall-through with an empty environment; • the counter; • 500 on a closed database; • the osctrld rate-limit budget; • an unknown-environment enroll creating no node and no audit row; • the enrolls-disabled cases. • The tests failed before the change, including the enrolls-disabled case (expected 403, got 503). •
go vet ./cmd/tls/...
,
go test ./...
and
go test -race ./cmd/tls/...
pass on an uncached run. • Not tested: the Redis-backed environment cache path. It was checked by reading the code: a Redis miss falls through to the database, and only a database "not found" counts as unknown. jmpsec/osctrl