Skip to content

Log Hygiene Policy ​

Status: Active. Owner: Privacy lead. Closes (partial): #307. Related: SEALED_IDENTITY.md (cross-cutting item 2 β€” "Define a centralized login-events policy").

1. Why this exists ​

GCP Cloud Logging does not support per-record deletion. Once PII is in logs, it stays until the bucket's retention period expires. That means GDPR Art. 17 ("right to be forgotten") and CCPA deletion requests cannot be honored against log data through any surgical mechanism β€” the only enforcement is (a) don't write PII in the first place, and (b) keep retention short enough that PII age out on a predictable schedule.

This policy codifies both.

2. What counts as PII for logging purposes ​

IdentifierLoggable?Notes
userId (Firebase Auth UID, opaque UUID)YesThe whole point of the architecture is that userId alone isn't directly identifying. Use it freely as the correlation key.
email (plaintext or normalized)NoNever.
phoneNumber (E.164 plaintext, normalized, or any partial form)NoNever.
phoneHash (post-sealed-identity Stage A)NoEquivalent to plaintext for correlation purposes — would enable post-hoc rebuild of the phone→userId link.
displayName / lanternNameNoUser-chosen handles. Treat as PII.
githubUsernameNoExternal identity link.
IP addressLimitedPino's HTTP middleware logs request IPs by default. We accept this in raw access logs as part of the GCP platform layer; do not propagate IP into application log fields.
userAgentLimitedSame as IP β€” platform-layer only, not application logs.
Error messages with stack tracesYesProvided they don't contain interpolated user values. Audit before logging.
targetUserId, callerUid, requestedBy, oldUserIdYesAll userId forms.

3. Logging surfaces ​

SurfaceProduction behaviorNotes
devLog(...) from services/functions/firebase/lib/devLog.jsSuppressed when GCLOUD_PROJECT === 'lantern-app-prod'Safe to include arbitrary context (including emails) β€” these never reach prod GCP logs. Still prefer userId form for consistency.
log.info / log.warn / log.error / log.debugAlways written to Cloud LoggingMust follow the PII rules above.
logger.info / logger.warn / logger.error (firebase-functions/v2)Always writtenSame rules.
req.log.* (pino via pino-http in services/api/*)Always written to stdout, captured by Cloud RunSame rules. Use structured key-value form so PII fields can be greppable in audits.
console.log / .warn / .errorCaptured by Cloud Functions / Cloud RunSame rules. Prefer log.* for intent clarity.

4. Retention policy ​

  • Cloud Logging default bucket retention: 30 days for all log sinks (Cloud Functions, Cloud Run services, Firebase Hosting access logs).
  • Configured manually via GCP Console β†’ Logging β†’ Log Router β†’ Edit retention. Not yet automated via IaC; tracked as a follow-up.
  • Required projects: lantern-app-prod, lantern-app-dev. Verify after rotation.
  • Exception: audit-significant events (admin actions, security incidents) can be exported to a longer-retention BigQuery sink with PII scrubbed at the sink level. Not yet implemented.

5. Audit results (2026-05-10) ​

This pass cleaned production-surface log statements that interpolated PII. Changes:

FileBeforeAfter
services/functions/firebase/modules/phoneRecycling.js:147logger.info('Phone reclaim initiated', { ..., phoneNumber: normalized, ... })phone field removed
services/functions/firebase/modules/phoneRecycling.js:264logger.info('Phone reclaim completed', { ..., phoneNumber: data.phoneNumber, ... })phone field removed
services/api/auth/src/routes/adminClaim.js:121req.log.info({ ..., email: normalizedEmail }, 'Admin role claimed successfully')email field removed
services/functions/firebase/modules/adminUsers.js:267–273logger.info('resendAdminSetupLink: Target user found', { ..., targetEmail: targetUser.email, ... })targetEmail removed
services/functions/firebase/modules/adminUsers.js:278–284logger.warn(..., { ..., targetEmail: targetUser.email, ... })targetEmail removed
services/functions/firebase/modules/userRoles.js:121log.warn(\Could not revoke GitHub access for @${githubUsername}:`, ...)`swapped to user ${targetUserId}
services/functions/firebase/modules/adminDeletion.js:92same pattern as above with userIdswapped to user ${userId}

devLog(...) calls that interpolate email or display name are unchanged β€” they are suppressed in production by the isDevelopment check in devLog.js, so they never reach Cloud Logging. They still appear during local development and emulator runs, which is the intended behavior.

6. What's still out of scope ​

These are tracked separately:

  • Firestore adminActions audit entries still contain targetEmail and githubUsername for some actions (createAdminUser, deleteAdminUser, resendAdminSetupLink, claimAdminRole). Firestore supports per-document deletion, so this is GDPR-tractable β€” but it's still PII at rest. Will be addressed in #308 (GDPR unified account deletion) cleanup, not here. The principle is the same; the mechanism is different.
  • Cloud Logging retention enforcement via IaC. Currently a manual console toggle. Tooling work, not policy work.
  • Sealed-identity Stage B's "centralized login-events policy" (brief Β§3, cross-cutting item 2). Decision pending on whether to create a dedicated Firestore collection for login events vs. relying solely on platform access logs. Out of scope here.

7. Going forward ​

  • New code must follow Β§2 (loggable identifiers). Code reviews should grep for PII patterns before merge.
  • A pre-commit or pre-PR check could enforce this automatically (regex scan over services/** for the disallowed identifier names inside log calls). Not yet built; tracked as follow-up.
  • When the sealed-identity Stage A spec ships, the phoneHash field is added to the disallowed list and users.phone is removed entirely from the data model.

Built with VitePress