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 β
| Identifier | Loggable? | Notes |
|---|---|---|
userId (Firebase Auth UID, opaque UUID) | Yes | The whole point of the architecture is that userId alone isn't directly identifying. Use it freely as the correlation key. |
email (plaintext or normalized) | No | Never. |
phoneNumber (E.164 plaintext, normalized, or any partial form) | No | Never. |
phoneHash (post-sealed-identity Stage A) | No | Equivalent to plaintext for correlation purposes β would enable post-hoc rebuild of the phoneβuserId link. |
displayName / lanternName | No | User-chosen handles. Treat as PII. |
githubUsername | No | External identity link. |
IP address | Limited | Pino'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. |
userAgent | Limited | Same as IP β platform-layer only, not application logs. |
| Error messages with stack traces | Yes | Provided they don't contain interpolated user values. Audit before logging. |
targetUserId, callerUid, requestedBy, oldUserId | Yes | All userId forms. |
3. Logging surfaces β
| Surface | Production behavior | Notes |
|---|---|---|
devLog(...) from services/functions/firebase/lib/devLog.js | Suppressed 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.debug | Always written to Cloud Logging | Must follow the PII rules above. |
logger.info / logger.warn / logger.error (firebase-functions/v2) | Always written | Same rules. |
req.log.* (pino via pino-http in services/api/*) | Always written to stdout, captured by Cloud Run | Same rules. Use structured key-value form so PII fields can be greppable in audits. |
console.log / .warn / .error | Captured by Cloud Functions / Cloud Run | Same 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:
| File | Before | After |
|---|---|---|
services/functions/firebase/modules/phoneRecycling.js:147 | logger.info('Phone reclaim initiated', { ..., phoneNumber: normalized, ... }) | phone field removed |
services/functions/firebase/modules/phoneRecycling.js:264 | logger.info('Phone reclaim completed', { ..., phoneNumber: data.phoneNumber, ... }) | phone field removed |
services/api/auth/src/routes/adminClaim.js:121 | req.log.info({ ..., email: normalizedEmail }, 'Admin role claimed successfully') | email field removed |
services/functions/firebase/modules/adminUsers.js:267β273 | logger.info('resendAdminSetupLink: Target user found', { ..., targetEmail: targetUser.email, ... }) | targetEmail removed |
services/functions/firebase/modules/adminUsers.js:278β284 | logger.warn(..., { ..., targetEmail: targetUser.email, ... }) | targetEmail removed |
services/functions/firebase/modules/userRoles.js:121 | log.warn(\Could not revoke GitHub access for @${githubUsername}:`, ...)` | swapped to user ${targetUserId} |
services/functions/firebase/modules/adminDeletion.js:92 | same pattern as above with userId | swapped 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
adminActionsaudit entries still containtargetEmailandgithubUsernamefor 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
phoneHashfield is added to the disallowed list andusers.phoneis removed entirely from the data model.