fix(privacy): prevent student-data leakage through logs and observability - #39
Merged
Merged
Conversation
…lity Closes DogStark#26. Structured logs previously carried raw student_id and attempted word values in messages and JSON fields, creating a shadow store of children's activity outside export_student_data, delete_student_data, and the retention sweep. - log_config: keyed HMAC-SHA256 pseudonymization (LOG_PSEUDONYM_KEY, environment-specific and rotatable; per-process random key fallback); never an unsalted public hash - log_config: RedactionFilter scrubbing formatted messages, plain templates, exception text, bearer/authorization headers, labeled identifier/content fields, and URL path segments in both logging modes - log_config: RFC 3339 UTC JSON timestamps and a non-identifying field allowlist that transforms legacy student_id/word extras defensively - main: request/correlation-ID middleware emitting method, route template, status, latency, and outcome; X-Request-ID response header; storage-error handler no longer echoes raw exception detail - routes/hint_generator/story_mode: logs carry only pseudonymous refs, length buckets, counts, error types, and enumerated outcomes - PRIVACY.md: data-classification policy, pseudonymization key handling, and operator obligations for external log retention/access - tests/test_log_privacy.py: capture-based regression tests asserting seeded keys/IDs/guardian IDs/words/story text never appear in either log mode, plus timestamp validity, correlation-ID propagation, and route-template redaction checks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Closes #26
Structured logs previously included raw
student_idand attemptedwordvalues in messages and JSON fields. Because log sinks are shipped to third-party observability systems with retention/access rules that differ from the primary student store — and sit outsideexport_student_data,delete_student_data, and the documented retention sweep — they formed a shadow record of children's activity that application deletion could never purge.This PR makes logs safe by design: identifiers are pseudonymized, learning content is reduced to bounded categorical fields, a redaction filter scrubs every emitted record as defense in depth, and the policy is documented and regression-tested.
Changes
agent/log_config.py(core)pseudonymize): truncated HMAC-SHA256 keyed byLOG_PSEUDONYM_KEY. The key is environment-specific and rotatable; rotating it permanently unlinks all historical correlation values. When unset, a random per-process key is used so values never survive restarts. Unsalted public hashes are never used.RedactionFilteron every handler: scrubs formatted messages, plain message templates, pre-rendered exception text,Bearer/authorization headers, labeled fields (student_id=…,word=…,api_key=…,guardian_id=…), and identifier-bearing URL path segments (covers third-party access loggers such asuvicorn.access/httpx) →[REDACTED].2026-08-21T20:11:45.935Z), validated by tests viadatetime.fromisoformat+ UTC-offset assertions.student_idbecomes a pseudonymousstudent_ref; straywordbecomesword_length_bucket(short|medium|long).main.pyX-Request-ID), plus one bounded access-log line per request carrying method, route template (never raw paths/query strings), status code, latency_ms, and outcome category.api/routes.pyPOST /profile: raw ID replaced withstudent_refpseudonym +outcome.POST /attempt: word/student removed from message and extras; nowstudent_ref,word_length_bucket, success/failure outcome, response time metric.POST /hint: word removed; now length bucket, attempt number, andprovider_outcome(generated|fallback).agent/hint_generator.py/agent/story_mode.pyprovider_unavailable|output_rejected|provider_error) and bounded counts — provider payloads, words, and story/hint text are never echoed.PRIVACY.mdtests/test_log_privacy.py(new, 26 tests)Capture-based regression tests attach to the real pipeline (both
LOG_JSON=0andLOG_JSON=1) and assert seeded API keys, student IDs, guardian IDs, attempted words, and generated story text never appear across:plus pseudonymization properties (stability, rotation, environment separation, never an unsalted hash), timestamp validity, request-ID correlation between response header and logs, and route-template substitution for raw URLs.
One pre-existing test (
TestBedrockExceptionLogging) asserted the old leaky behavior (wordpresent in provider-error logs); it was flipped to enforce the new policy.Acceptance criteria mapping
tests/test_log_privacy.py::TestApiPathsDoNotLeakStudentDatapseudonymize()+LOG_PSEUDONYM_KEYrotation tests;X-Request-IDmiddlewareTestJsonTimestampsPRIVACY.md“Logging and observability”Verification
ruff check .— cleanmypy— cleanpytest --cov=agent --cov=api --cov=dashboard --cov=main --cov-branch— 178 passed; coverage 84% (gate ≥80%)GET / -> 200with correlation ID; startup sweep unaffectedNotes for reviewers
LOG_PSEUDONYM_KEYmust be provisioned like a secret per deployment; unset it and correlation values become process-ephemeral (documented).