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.

7 min read Security & Infrastructure As of Feb 9, 2026

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.

Encrypted Fields

Model Field Data Type
HOA controller_ssn Social Security Number
HOA controller_dob Date of Birth
BankAccount Various credential fields Bank account identifiers

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
  • 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
  • Request body data
  • User identifiers

Any PII that appears in Sentry events is explicitly added by CommunityPay's instrumentation code — and then filtered by the layers below.

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: 17 sensitive field patterns are checked against request body data:

  • 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

Any matching field is replaced with [FILTERED] before the event is transmitted.

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 username portion is stripped because it often contains personally identifiable information.

Extra context filtering: Any key in the Sentry event's extra context that matches a sensitive field pattern 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 stack trace from a payment failure will not contain the bank account number from the request body
  • An unhandled exception during user registration will not expose the password from the POST data
  • A Stripe webhook processing error will not leak the webhook signature from the request headers
  • A Celery task failure processing an HOA controller's KYC will not expose their SSN

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

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

CommunityPay strips information-leaking headers from all 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

Server and X-Powered-By headers are actively stripped 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.

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 pattern, no CAPTCHA fallback, and no way to bypass the lockout through the application layer.

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') - Stripe.js (js.stripe.com, hooks.stripe.com) - A per-request cryptographic nonce for inline scripts

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.

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. Style hardening is a separate task tracked independently.

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
  • KYC events record the verification status and timestamp — not the SSN or date of birth
  • Webhook events record the provider, event type, and processing result — not the webhook payload

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 SSN, date of birth, and bank credential fields at the model layer
  • Sentry before_send hook filters 17+ sensitive field patterns from request data, headers, and extra context
  • 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 stripped from all responses to reduce fingerprinting surface
  • Health check, ping, and static file transactions excluded from Sentry to reduce noise and cost
Login