Security & SOC 2 Compliance¶
CampusCore operates under SOC 2 Type II requirements (Trust Services Criteria CC6.1, CC6.7). This document covers the encryption and data protection architecture.
Encryption at Rest¶
All AWS storage resources use encryption at rest with AWS-managed keys. No application code changes are needed — encryption is transparent at the storage layer.
| Resource | Method | Terraform Config |
|---|---|---|
| RDS PostgreSQL | AWS-managed KMS key (storage_encrypted = true) |
infrastructure/modules/rds/main.tf |
| S3 Buckets | SSE-S3 (AES-256) via aws_s3_bucket_server_side_encryption_configuration |
infrastructure/modules/s3/main.tf |
| SQS Queues | SSE-SQS (sqs_managed_sse_enabled = true) |
infrastructure/modules/sqs/main.tf |
| SSM Parameters | KMS SecureString (AWS default) | infrastructure/base/main.tf |
Impact on application code¶
None. Storage-level encryption is transparent to PostgreSQL, boto3, and Django. Queries, S3 uploads/downloads, SQS messages, and pgvector HNSW indexes all work identically — AWS encrypts/decrypts at the block storage layer before data reaches the application.
Encryption in Transit¶
Database connections¶
Cloud database connections enforce TLS via sslmode=require in Django's DATABASES config, gated on IS_CLOUD_ENV. RDS provisions TLS certificates by default — the setting ensures the connection is encrypted.
Local development (Docker Compose) is unaffected because IS_CLOUD_ENV=False.
Config: campuscore_app/campus_core/settings.py — OPTIONS dict in DATABASES.
HTTPS and security headers¶
In cloud environments (IS_CLOUD_ENV=True), Django enforces:
| Setting | Value | Purpose |
|---|---|---|
SECURE_SSL_REDIRECT |
True |
HTTP → HTTPS redirect |
SECURE_REDIRECT_EXEMPT |
[health_check, readiness] |
ALB/ECS health probes use HTTP |
SESSION_COOKIE_SECURE |
True |
Session cookies only sent over HTTPS |
CSRF_COOKIE_SECURE |
True |
CSRF cookies only sent over HTTPS |
SESSION_COOKIE_HTTPONLY |
True |
Prevents JavaScript access to session cookie |
SECURE_HSTS_SECONDS |
31536000 (1 year) |
Strict Transport Security header |
SECURE_HSTS_INCLUDE_SUBDOMAINS |
True |
HSTS applies to all subdomains |
SECURE_PROXY_SSL_HEADER is set to trust the ALB's X-Forwarded-Proto header, which prevents infinite redirect loops behind the load balancer.
Note: HSTS preload is intentionally not enabled — preload list submission is near-irreversible (6+ month removal process). This will be reconsidered once all client domains are stable long-term.
Config: campuscore_app/campus_core/settings.py — IS_CLOUD_ENV gated block.
Network Edge Protection¶
Public ALBs receive continuous automated bot traffic: PHPUnit exploit probes, .env exfiltration attempts, EC2 IMDS SSRF probes, log4shell payloads. CampusCore attaches an AWS WAFv2 Web ACL to the ALB that drops these at the edge before they reach ECS. This reduces app-server load, eliminates the observability noise these requests would generate in Sentry, and prevents new dependency CVEs from being trivially exploitable against the public surface.
Web ACL rules¶
| Rule | Type | What it catches | WCUs |
|---|---|---|---|
AWSManagedRulesCommonRuleSet |
AWS managed | OWASP-style: SQLi, XSS, traversal, generic injection | 700 |
AWSManagedRulesKnownBadInputsRuleSet |
AWS managed | log4shell, IMDS SSRF, known exploit payloads | 200 |
AWSManagedRulesPHPRuleSet |
AWS managed | PHPUnit RCE probes, PHP injection patterns | 100 |
AWSManagedRulesAmazonIpReputationList |
AWS managed | Known-bad IP reputation list | 25 |
RateLimitPerIP |
Custom rate-based | >2000 requests per 5-min from a single IP | 2 |
Total: ~1027 / 1500 WCU budget.
Config: infrastructure/modules/waf/main.tf, associated with the ALB in infrastructure/base/main.tf.
Tuning notes¶
SizeRestrictions_BODY(part ofCommonRuleSet) is overridden to count instead of block — CampusCore's chat endpoints and document uploads legitimately exceed 8KB, so the default action would 403 real users. The override keeps the metric for visibility without blocking traffic.- New rule additions must fit within the 1500 WCU budget or the ACL won't validate. Check
vendor_name + namegroup sizes in the AWS WAF console before adding. - Bot Control is intentionally not enabled: it would add ~700 WCUs plus $10/month + $1/M requests, and the basic managed rules already neutralize the noise we observe.
Request logging¶
Off by default (enable_logging = false in the WAF module). When enabled:
- Sends per-request structured logs to a CloudWatch log group named
aws-waf-logs-campuscore-{env}(theaws-waf-logs-prefix is required by WAF'sPutLoggingConfigurationAPI) - The logging filter is
default_behavior = "DROP"withKEEPforBLOCKandCOUNTactions — legitimateALLOWtraffic never reaches the log group, keeping cost bounded - Use only during active incident triage; CloudWatch metrics + the WAF console's sampled-requests view cover routine operations
Cost¶
Approximately $10/month per environment at pilot scale: $5 Web ACL + $5 for five rules + $0.60 per million requests inspected. Logging adds $0.50/GB ingest and $0.03/GB storage when enabled (typically under $3/month even during incidents).
Application-Level Encryption¶
Connector credentials¶
External service credentials (OAuth tokens, API keys for Google Drive, Canvas, etc.) are encrypted at the application layer using Fernet symmetric encryption before storage in the database.
- Implementation:
apps/connectors/services/secrets.py—encrypt_payload(),decrypt_payload() - Key:
APP_FERNET_KEYenvironment variable, generated viapython manage.py generate_fernet_key - Scope:
Connection.credentialsandConnectorConfig.config_value(whenis_secret=True)
Sensitive Data Handling¶
CampusCore handles data subject to FERPA (Family Educational Rights and Privacy Act):
What counts as sensitive: - Student PII (names, emails, student IDs, enrollment data) - Authentication credentials (passwords, session tokens, SAML assertions) - Chat conversations (may contain personal questions about financial aid, grades, health services) - FERPA-protected education records (grades, disciplinary records, financial aid) - Knowledge base content sourced from non-public university systems
Rules:
- Sensitive data must not appear in logs, error messages, or stack traces
- Connector credentials must use the Fernet encryption layer, never plaintext DB fields
- Views handling sensitive data must use appropriate auth decorators (@htmx_login_required, @registered_user_required, @htmx_staff_member_required)
FERPA scrubbing for Sentry events¶
When Sentry is enabled, error events, transactions, and logs are routed through three before_send_* hooks that redact and truncate before the SDK ships them off-tenant. The scrubber is the last line of defense against PII reaching Sentry — code review is still expected to keep PII out of log calls and exception messages at the source.
Location: campuscore_app/campus_core/observability/sentry_scrubber.py. Wired in sentry_setup.py::init_sentry.
| Hook | Sentry data path | What this scrubber does |
|---|---|---|
before_send |
Error events + breadcrumbs | Walks extra, contexts, tags, breadcrumbs, request, exception dicts and runs every string value through the redaction regexes. Drops events whose request URL contains a path in _DROP_PATH_PREFIXES (/health_check, /static/, /favicon.ico). |
before_send_log |
Sentry Logs product entries | Scrubs the log body plus attributes. |
before_send_transaction |
Performance traces | Scrubs every span's data dict and description. |
Redaction patterns (compiled once at import time, applied to every string value):
- Email-shaped: [A-Za-z0-9._%+\-]+@[A-Za-z0-9.\-]+\.[A-Za-z]{2,}
- Phone-shaped: 10-digit North American with separators and optional +1 prefix
- SSN-shaped: \d{3}-\d{2}-\d{4}
Matches are replaced with the literal token [redacted]. The patterns are intentionally broad — over-redaction in Sentry is preferable to leaking PII.
Field truncation (large free-text fields capped to a byte ceiling before ship):
- gen_ai.prompt, campuscore.agent.system_prompt, campuscore.agent.dynamic_context → 2,000 chars
- gen_ai.tool.input, gen_ai.tool.output, tool.arguments, tool.result_content → 1,500 chars
This keeps Sentry payloads under their 100 KB/event limit and avoids leaking long prompts containing student data into the Sentry UI.
Extending the scrubber: add a new pattern to the constants at the top of the file (_EMAIL_RE, _PHONE_RE, etc.) or add an entry to _TRUNCATE_FIELDS. Keep regexes compiled at module level — before_send_log runs on Sentry's background worker thread for every log; a slow scrubber backs up the queue and causes drops.
Defensive posture: the scrubber should never raise on a malformed log. If a future change introduces a code path that could raise, wrap the scrubber body in try/except returning the original payload on failure — losing event detail is better than silently dropping events. See Sentry Setup → Troubleshooting for diagnosing scrubber drops.
Audit checklist for code review¶
When reviewing code that touches logs or Sentry:
- New logger.error(...) or logger.warning(...) calls — do they format student data into the message?
- New extra={...} kwargs on log calls — do they pass raw FERPA fields?
- New sentry_sdk.capture_exception() or capture_message() calls — would they include sensitive request data?
- Exception messages raised from views handling student data — should they be sanitize_error'd before re-raising?
The scrubber will catch obvious leaks, but the goal is to not need it.
CI Enforcement¶
Terraform encryption policy check¶
scripts/check-encryption-policies.sh runs on every PR that touches infrastructure/ via .github/workflows/infra-validation.yml. It fails the build if:
- Any
aws_s3_bucketresource lacks a matchingaws_s3_bucket_server_side_encryption_configuration - Any
aws_sqs_queueresource lackssqs_managed_sse_enabled - Any
aws_db_instanceresource lacksstorage_encrypted
Coding agent enforcement¶
CLAUDE.md contains a "SOC 2 Compliance — Security by Default" section under Coding Conventions. This ensures the coding agent considers encryption and data protection requirements when writing new infrastructure or application code.
Adding New AWS Resources¶
When adding new storage resources to Terraform:
- S3 buckets — add an
aws_s3_bucket_server_side_encryption_configurationresource (AES-256) and anaws_s3_bucket_public_access_block - RDS instances — include
storage_encrypted = true - SQS queues — include
sqs_managed_sse_enabled = true - New storage types (DynamoDB, EFS, etc.) — enable encryption at rest using AWS-managed keys
- Public-facing load balancers or CloudFront distributions — attach an
aws_wafv2_web_acl_associationto the existing WAF Web ACL (REGIONALscope for ALBs,CLOUDFRONTscope for distributions). Public ingress without WAF is a SOC 2 finding.
Use AWS-managed keys unless there's a specific requirement for customer-managed KMS keys. The CI policy check will catch missing encryption configs before merge.