GitHub
11/08/2026, 2:19 PM200 {"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/osctrlGitHub
11/08/2026, 3:02 PM