Skip to content

Commit fb46a3f

Browse files
committed
fix: bound authenticated request handling
1 parent a45ab8e commit fb46a3f

3 files changed

Lines changed: 32 additions & 5 deletions

File tree

‎docs/STATUS-REPORTING.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ Covered threats:
104104
Residual risks:
105105

106106
- compromise of one controller exposes that controller's reporting key and permits forged reports for that identity until rotation;
107+
- ordinary runner jobs are already host-root-equivalent through the Docker socket and can read any host-local reporting key; this channel prevents cross-controller impersonation but does not attest a controller against malicious code already running on that controller. Isolate job Docker onto a separate trust boundary before treating reports as adversarial to job code;
107108
- compromise of the receiver exposes retained status and all receiver-side reporting keys;
108109
- HMAC keys are symmetric; use a managed asymmetric identity service later if receiver compromise becomes part of the impersonation threat model;
109110
- the read bearer token is suitable for the backend foundation, not browser distribution. A future console should terminate user authentication before this API.

‎scripts/status_receiver.py‎

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def __init__(
5858
self.max_clock_skew_seconds = max_clock_skew_seconds
5959
# ponytail: one receiver-wide lock; split by controller only if measured write contention warrants it.
6060
self._write_lock = threading.Lock()
61+
self._last_attempt: dict[str, int] = {}
6162
self._clock = time.time
6263
with self._connect() as connection:
6364
connection.executescript("""
@@ -112,6 +113,12 @@ def submit(self, body: bytes, headers: Mapping[str, str], *, now: int) -> None:
112113
raise StatusError(409, "report_time_stale")
113114
encoded = json.dumps(report, separators=(",", ":"), sort_keys=True)
114115
with self._write_lock, self._connect() as connection:
116+
if connection.execute("SELECT 1 FROM nonces WHERE controller=? AND nonce=?", (controller, nonce)).fetchone():
117+
raise StatusError(409, "replayed_report")
118+
last_attempt = self._last_attempt.get(controller)
119+
if last_attempt is not None and now - last_attempt < 1:
120+
raise StatusError(429, "submission_too_frequent")
121+
self._last_attempt[controller] = now
115122
try:
116123
connection.execute("INSERT INTO nonces VALUES (?, ?, ?)", (controller, nonce, authenticated_at))
117124
connection.commit()
@@ -160,6 +167,9 @@ def integer(value: Any, minimum: int = 0) -> bool:
160167
def number(value: Any, minimum: float = 0) -> bool:
161168
return isinstance(value, (int, float)) and not isinstance(value, bool) and value >= minimum and value < float("inf")
162169

170+
def enum(value: Any, choices: set[str]) -> bool:
171+
return isinstance(value, str) and value in choices
172+
163173
if not isinstance(report, dict) or type(report.get("schema_version")) is not int or report["schema_version"] != 1:
164174
raise StatusError(400, "unsupported_schema")
165175
root_keys = {"schema_version", "controller", "configuration", "reconciliation", "drift", "process", "timers", "runners", "metrics", "docker", "error", "generated_at"}
@@ -170,7 +180,7 @@ def number(value: Any, minimum: float = 0) -> bool:
170180
raise StatusError(403, "controller_identity_mismatch")
171181
if not isinstance(identity["software_version"], str) or not re.fullmatch(r"[A-Za-z0-9_.+-]{1,64}", identity["software_version"]):
172182
raise StatusError(400, "invalid_report")
173-
if not integer(identity["boot_time"]) or identity["ssh"] not in {"enabled", "disabled", "unknown"}:
183+
if not integer(identity["boot_time"]) or not enum(identity["ssh"], {"enabled", "disabled", "unknown"}):
174184
raise StatusError(400, "invalid_report")
175185
generated_at = report["generated_at"]
176186
if not integer(generated_at) or identity["boot_time"] > generated_at:
@@ -182,17 +192,17 @@ def number(value: Any, minimum: float = 0) -> bool:
182192
raise StatusError(400, "invalid_report")
183193
reconciliation = report["reconciliation"]
184194
reconcile_states = {"bootstrap", "converged", "drift", "failed", "invalid", "missing", "pending", "reconciling", "rolled_back", "unknown"}
185-
if not exact(reconciliation, {"state", "last_success_at"}) or reconciliation["state"] not in reconcile_states:
195+
if not exact(reconciliation, {"state", "last_success_at"}) or not enum(reconciliation["state"], reconcile_states):
186196
raise StatusError(400, "invalid_report")
187197
if reconciliation["last_success_at"] is not None and (not integer(reconciliation["last_success_at"]) or reconciliation["last_success_at"] > generated_at):
188198
raise StatusError(400, "invalid_report")
189-
if not exact(report["drift"], {"state"}) or report["drift"]["state"] not in {"ok", "stale", "failed", "unknown"}:
199+
if not exact(report["drift"], {"state"}) or not enum(report["drift"]["state"], {"ok", "stale", "failed", "unknown"}):
190200
raise StatusError(400, "invalid_report")
191201
process = report["process"]
192-
if not exact(process, {"state", "restart_count"}) or process["state"] not in {"created", "exited", "missing", "paused", "restarting", "running", "unknown"} or not integer(process["restart_count"]):
202+
if not exact(process, {"state", "restart_count"}) or not enum(process["state"], {"created", "exited", "missing", "paused", "restarting", "running", "unknown"}) or not integer(process["restart_count"]):
193203
raise StatusError(400, "invalid_report")
194204
timers = report["timers"]
195-
if not exact(timers, {"reconciliation", "drift", "health", "cleanup"}) or any(value not in {"ok", "stale", "failed", "unknown"} for value in timers.values()):
205+
if not exact(timers, {"reconciliation", "drift", "health", "cleanup"}) or any(not enum(value, {"ok", "stale", "failed", "unknown"}) for value in timers.values()):
196206
raise StatusError(400, "invalid_report")
197207
runners = report["runners"]
198208
if not exact(runners, {"current", "busy", "maximum"}) or not all(integer(value) for value in runners.values()) or not (runners["busy"] <= runners["current"] <= runners["maximum"]):

‎scripts/test_status_receiver.py‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,10 @@ def test_payload_and_submission_frequency_are_bounded(self) -> None:
181181
limited.submit(body, headers, now=1_000)
182182
second_body, second_headers = self.signed(valid_report(generated_at=1_001), timestamp=1_001, nonce="b" * 32)
183183
self.assert_status_error(429, "submission_too_frequent", lambda: limited.submit(second_body, second_headers, now=1_001))
184+
third_body, third_headers = self.signed(valid_report(generated_at=1_002), timestamp=1_002, nonce="c" * 32)
185+
self.assert_status_error(429, "submission_too_frequent", lambda: limited.submit(third_body, third_headers, now=1_001))
186+
with sqlite3.connect(limited.database) as connection:
187+
self.assertEqual(connection.execute("SELECT COUNT(*) FROM nonces").fetchone()[0], 2)
184188
self.assert_status_error(409, "replayed_report", lambda: limited.submit(second_body, second_headers, now=1_031))
185189

186190
def test_schema_compatibility_and_malformed_metrics(self) -> None:
@@ -194,6 +198,18 @@ def test_schema_compatibility_and_malformed_metrics(self) -> None:
194198
body, headers = self.signed(boolean, nonce="f" * 32)
195199
self.assert_status_error(400, "unsupported_schema", lambda: self.receiver.submit(body, headers, now=1_000))
196200

201+
for mutate in (
202+
lambda report: report["controller"].update(ssh={}),
203+
lambda report: report["reconciliation"].update(state=[]),
204+
lambda report: report["drift"].update(state={}),
205+
lambda report: report["process"].update(state=[]),
206+
lambda report: report["timers"].update(health={}),
207+
):
208+
malformed = valid_report()
209+
mutate(malformed)
210+
body, headers = self.signed(malformed, nonce="e" * 32)
211+
self.assert_status_error(400, "invalid_report", lambda body=body, headers=headers: self.receiver.submit(body, headers, now=1_000))
212+
197213
malformed = valid_report()
198214
malformed["metrics"]["memory"]["available_bytes"] = -1
199215
body, headers = self.signed(malformed, nonce="b" * 32)

0 commit comments

Comments
 (0)