Data Residency & Privacy Controls

PII field-level encryption, Sentry before_send filtering, session security, error reporting hygiene, and the middleware chain that strips sensitive data before it leaves the application boundary.

9 min read Security & Infrastructure Published Aug 29, 2024

CommunityPay handles sensitive personal and financial data — Social Security numbers, bank account details, dates of birth, and payment credentials. This page describes how that data is protected at rest, in transit, and in error reporting pathways.

Field-Level Encryption

Sensitive fields are encrypted at the model layer using Fernet symmetric encryption before being written to the database. Fernet is symmetric, authenticated encryption.

Encrypted Fields

Model Field Data Type
HOA controller_ssn Social Security Number of the association's controller
HOABeneficialOwner ssn Social Security Number
Profile ssn Social Security Number
Vendor bank_routing_number, bank_account_number A vendor's bank routing and account numbers
HOAClaimFormSubmission payout_routing_number, payout_account_number Bank routing and account numbers submitted on a claim form

The controller's date of birth is stored as a date and is not encrypted. The association's own bank is identified by a provider token and its last four digits, not by account numbers held in the database. The table lists the encrypted fields in the live data model. The bank-file records of a payment rail that is not live also encrypt their receiver account and routing numbers.

Encryption happens transparently through Django encrypted model fields. Application code reads and writes plaintext values; the encryption and decryption occur at the ORM boundary. The encryption key is loaded from environment variables and is never stored in the codebase.

What This Means in Practice

  • Database dumps contain ciphertext, not plaintext SSNs or vendor bank numbers
  • Database administrators with read access see encrypted values
  • Backup restoration preserves encryption (the key is separate from the data)
  • Log files that capture model field values capture ciphertext

The encryption is symmetric (Fernet), which means the same key encrypts and decrypts. Key rotation requires re-encrypting existing records, which is a controlled migration operation.

Error Reporting Hygiene

CommunityPay uses Sentry for error tracking and performance monitoring. Error reports can inadvertently contain sensitive data from request payloads, headers, or application context. The platform applies multiple layers of filtering before any data reaches Sentry.

Layer 1: Default PII Exclusion

The Sentry SDK is configured with send_default_pii=False. This means Sentry does not automatically capture:

  • User IP addresses
  • User cookies

The application attaches the signed-in user's numeric id to each event on purpose, so an error can be matched to an account; the user's email and username are masked as described below. A request body, when an event carries one, passes through the layers below before the event leaves the application.

Layer 2: before_send Hook

Every Sentry event passes through a before_send function before leaving the application. This function applies three categories of filtering:

Request data filtering: 21 sensitive field names are compared with the keys of the request body. A key equal to one of these names is replaced; a key that only contains one, such as bank_account_number or password1, is not matched:

  • password, secret, token, api_key, access_key, private_key
  • ssn, social_security, ein
  • bank_account, routing_number, account_number
  • stripe_customer, dwolla_customer, plaid_access_token
  • card_number, cvv, pin
  • date_of_birth, dob, birth_date

A matching key's value is replaced with [FILTERED] before the event is transmitted. The hook does not rewrite values held in stack frames.

Header filtering: Authorization, cookie, API key, and webhook signature headers are replaced with [FILTERED]. This prevents authentication tokens and webhook verification secrets from appearing in error reports.

Email masking: User email addresses are masked to ***@domain. The domain is preserved because it aids debugging (distinguishing internal vs. external users, identifying which organization a user belongs to). The local part is stripped because it often contains personally identifiable information. A username that is itself an email address is masked the same way; the numeric user id is kept.

Extra context filtering: Any key in the Sentry event's extra context that contains one of the 21 sensitive words, in any letter case, is replaced with [FILTERED]. This catches cases where application code adds debug context that inadvertently includes sensitive data.

Layer 3: Transaction Filtering

The before_send_transaction function excludes routine transactions from Sentry:

  • Health check endpoints (/health/, /ping/, /robots.txt)
  • Static file requests (/static/...)

These requests generate high volume with low diagnostic value. Excluding them reduces Sentry event consumption and keeps the error stream focused on application issues.

What This Architecture Prevents

  • A request body key named exactly like a sensitive field, such as password or ssn, is replaced before the report leaves the application
  • A Stripe webhook processing error will not leak the webhook signature from the request headers
  • A context key on an error report that is named like a sensitive field is replaced before the report leaves the application

Session Security

Session configuration enforces strict boundaries on session lifetime and cookie behavior:

Setting Value Purpose
SESSION_COOKIE_AGE 3,600 seconds (1 hour) Maximum session duration
SESSION_EXPIRE_AT_BROWSER_CLOSE True Sessions end when browser closes
SESSION_COOKIE_HTTPONLY True JavaScript cannot access session cookie
SESSION_COOKIE_SECURE True (production) Cookie only transmitted over HTTPS
CSRF_COOKIE_HTTPONLY True JavaScript cannot access CSRF cookie
CSRF_COOKIE_SECURE True (production) CSRF cookie only over HTTPS
SESSION_COOKIE_SAMESITE Lax (production) Cookies are not sent on cross-site subrequests. Lax still allows top-level navigations, which OAuth sign-in requires

Why 60-Minute Sessions

HOA financial operations are high-value targets. A forgotten session on a shared computer is a vector for unauthorized access. One hour balances usability (a board member reviewing financial reports does not need to re-authenticate every 15 minutes) with security (an abandoned session expires before the next business day).

Response Header Hygiene

The application strips information-leaking headers from its HTTP responses:

Action Header Purpose
Remove Server Hides web server software and version
Remove X-Powered-By Hides application framework
Add X-Content-Type-Options: nosniff Prevents MIME type sniffing
Add X-XSS-Protection: 1; mode=block Legacy XSS protection (defense in depth)
Add Referrer-Policy: strict-origin-when-cross-origin Limits referrer information leakage
Add Expect-CT: max-age=86400, enforce Certificate Transparency enforcement. Current browsers ignore it

The application removes the Server and X-Powered-By headers from its responses because they reveal implementation details useful for targeted attacks. Knowing that an application runs on a specific web server version or framework narrows the attacker's search space for known vulnerabilities. The edge network in front of the application adds its own Server header.

Brute-Force Protection

Authentication endpoints are protected by django-axes:

Setting Value
Failure limit 5 attempts
Cooloff time 30 minutes
Lockout method IP-based

After 5 failed login attempts from the same IP, the IP is locked out for 30 minutes. This applies uniformly. There is no escalating delay and no CAPTCHA fallback.

All authentication events — successful logins, failed attempts, and lockouts — are logged to the audit trail.

Content Security Policy

CSP enforcement varies by route type:

Application Routes (Strict)

Script sources are restricted to:

  • The application's own domain ('self')
  • A short list of CDNs that serve UI framework libraries
  • Stripe.js (js.stripe.com)
  • The browser SDK for error reporting
  • A per-request cryptographic nonce for inline scripts

Frames are restricted to the application and Stripe's domains. Inline scripts without a valid nonce are blocked by the browser. The nonce is generated per request using 16 cryptographically random bytes, base64-encoded. It is available in templates as request.csp_nonce.

Public Pages Served From the Cache (Hashes)

A public page served from the shared cache cannot carry a per-request nonce, because the cache stores the whole response and would replay one nonce to every visitor. A nonce that is public and constant is not a nonce. These pages carry a SHA-256 hash of each inline script instead, computed from the exact body being sent, so a hash stays valid for exactly as long as the cached body does.

Admin and Dashboard Routes (Relaxed)

Admin and Plotly Dash routes require unsafe-inline for script-src because they inject inline scripts that cannot be nonce-tagged. These routes maintain CSP for other directives but relax the inline script restriction.

Style Sources

unsafe-inline is permitted for style-src across all routes. This is a known relaxation required by framework dependencies.

Two-Factor Sign-In

A signed-in account can enroll an authenticator app and recovery codes. A text-message code never confirms an action that moves money. A fresh code, confirmed within the last ten minutes, is asked for before paying a vendor by transfer, changing a vendor's bank, or changing who holds authority. A new authenticator cannot confirm a money action until the account's owner approves it from an email sent to the account's address. See Security & Trust for the full list of actions.

Permissions Policy

Browser capabilities are restricted via Permissions-Policy headers:

Capability Setting
Geolocation Disabled entirely
Microphone Disabled entirely
Camera Disabled entirely
USB Disabled entirely
Payment API Self + Stripe only

CommunityPay is a financial platform. It has no use for geolocation, microphone, camera, or USB access. Disabling these capabilities eliminates entire classes of potential abuse if the application were compromised.

The Payment API is restricted to the application's own origin and Stripe's domain. This allows Stripe Elements to function for payment processing while preventing any other origin from invoking the browser Payment API.

What We Don't Log

Audit logging captures the fact of an action, not the sensitive content. Specifically:

  • Login events record the user, timestamp, IP, and success/failure — not the password
  • Payment events record the amount, status, and payment method type — not the card number or bank account
  • Webhook entries in the audit trail record the provider, event type, and processing result. The webhook event store keeps the complete payload and signature so a duplicate delivery can be refused and a failed one replayed

This is a deliberate design choice. The audit trail exists to prove what happened and who did it. It does not need to reproduce the sensitive content that was involved.

How CommunityPay Enforces This
  • Fernet symmetric encryption on Social Security numbers and on vendor and claim-form bank routing and account numbers at the model layer
  • Sentry before_send hook replaces 21 named sensitive keys in request data, matches 21 sensitive words in extra context, and replaces authentication and signature headers
  • Email addresses masked to ***@domain in error reports — domain preserved for debugging, username stripped
  • SENTRY_SEND_DEFAULT_PII set to False — no PII transmitted unless explicitly included and filtered
  • Server and X-Powered-By headers removed from the application's responses to reduce fingerprinting surface
  • Health check, ping, and static file transactions excluded from Sentry to reduce noise and cost
Login