Skip to content

Security Checklist - Production Hardening Audit

Audit Date: January 10, 2026 Auditor: Engineer D (Production Hardening) Status: PASS with recommendations


1. Rate Limiting

Status: PASS

All public-facing endpoints have rate limiting via slowapi with Redis backing.

Route Category Rate Limit File Location
/auth/verify/start 5/minute app/api/auth_routes.py:73
/auth/verify/confirm 10/minute app/api/auth_routes.py:117
/auth/login 10/minute app/api/auth_routes.py:317
/auth/refresh 10/minute app/api/auth_routes.py:474
/payment/checkout/* 3/minute app/api/payment_routes.py:96,156,394
/account (DELETE) 3/hour app/api/account_routes.py:142
/account/export 2/day app/api/account_routes.py:337
/support/report-bug 5/hour app/api/support_routes.py:158
/support/feedback 10/hour app/api/support_routes.py:229
/support/ticket 5/hour app/api/support_routes.py:295

Redis Backing

  • Implementation: app/scheduler/redis_rate_limiter.py
  • Features:
  • Burst protection: 5 messages per 30 seconds
  • Minute limit: 10 messages/minute
  • Hourly limit: 60 messages/hour
  • Daily limit: 200 messages/day
  • Fail-open behavior: If Redis unavailable, allows requests (prevents outages from breaking the app)

Recommendations

  • Add rate limiting to /edge/* routes (currently relies on edge agent authentication)
  • Consider per-user rate limiting for authenticated endpoints (currently IP-based)

2. Input Validation

Status: PASS

All API endpoints use Pydantic models for request validation.

Examples:

# app/api/user_routes.py
class UpdateProfileRequest(BaseModel):
    name: Optional[str] = Field(None, max_length=100)
    pronouns: Optional[str] = Field(None, max_length=50)
    email: Optional[EmailStr] = None
    display_name: Optional[str] = Field(None, max_length=100)

Validation Coverage

  • String length limits on all text fields
  • Email validation via EmailStr
  • UUID validation for identifiers
  • Enum validation for status fields

3. SQL Injection Prevention

Status: PASS

All database operations use SQLAlchemy ORM with parameterized queries.

Query Pattern (Safe):

# Parameterized queries throughout codebase
user = db.query(User).filter_by(phone=user_phone).first()
db.query(User).filter(User.id == user_id).first()

No raw SQL with f-strings found in: - app/api/*.py - app/payment/service.py - app/oauth/routes.py - app/database/*.py


4. Authentication & Authorization

Status: PASS

JWT Token Security

  • Location: app/identity/session_manager.py
  • Algorithm: HS256
  • Access Token Expiry: 60 minutes
  • Refresh Token Expiry: 30 days
  • Validation: Proper expiry and signature verification
# Token verification includes expiration check
payload = jwt.decode(token, self.secret_key, algorithms=[self.algorithm])

Secret Key Validation

  • Production validation: Prevents default secret key in production
  • Location: app/config.py:57-66
@field_validator("secret_key")
def validate_secret_key(cls, v: str, info) -> str:
    if environment == "production" and v == "dev-secret-key-change-in-production":
        raise ValueError("Cannot use default SECRET_KEY in production!")

OAuth Token Encryption

  • Implementation: Fernet symmetric encryption
  • Location: app/oauth/token_manager.py
  • Key Management: FERNET_KEY environment variable

5. Webhook Signature Verification

Status: PASS

Stripe Webhooks

  • Location: app/payment/stripe_client.py:167-196
  • Method: stripe.Webhook.construct_event() with signature verification
event = stripe.Webhook.construct_event(
    payload, sig_header, self.webhook_secret
)

Twilio OTP Verification

  • Location: app/identity/twilio_client.py
  • Method: Twilio Verify API with service SID authentication
verification_check = self.client.verify.v2.services(
    self.verify_service_sid
).verification_checks.create(to=phone, code=code)

Edge Agent Authentication

  • Location: app/edge/auth.py, app/security/hmac_auth.py
  • Method: HMAC signature with timestamp validation

6. CORS Configuration

Status: PASS

  • Location: app/main.py:292-304, app/config.py:30-55
  • Implementation: Dynamic origins based on environment

Allowed Origins by Environment:

Environment Origins
Development localhost:3000, localhost:8502, archety-dev.vercel.app, archety-backend-dev.up.railway.app
Production archety.vercel.app, archety-web.vercel.app, archety-backend-prod.up.railway.app

Headers: - allow_credentials: True - allow_methods: ["GET", "POST", "PUT", "DELETE", "OPTIONS"] - allow_headers: ["Content-Type", "Authorization", "X-Requested-With", "X-CSRF-Token"]


7. CSRF Protection

Status: PASS

  • Location: app/security/csrf.py, app/middleware/csrf_middleware.py
  • Library: fastapi-csrf-protect
  • Configuration:
  • Cookie: httponly=True, samesite=lax
  • Secure cookie in production
  • Selective validation (web-facing endpoints only)

8. Sensitive Data in Logs

Status: PASS with Recommendations

Good Practices Found: - Phone numbers truncated: phone[-4:] (last 4 digits only) - User IDs truncated: user_id[:8]... (first 8 chars) - Tokens not logged in full

Examples:

logger.info(f"Created session for user {user_id[:8]}... (phone: {phone[-4:]})")
logger.info(f"Generated token for edge agent {edge_agent_id}")

Recommendations

  • Review app/oauth/routes.py:92-105 - logs full user_phone during OAuth flow
  • Consider masking middle digits of phone in all logs: +1***555***1234

9. Sentry Integration

Status: PASS

  • Location: app/main.py:186-203
  • Features:
  • FastAPI and Starlette integrations
  • Environment tagging
  • 10% sampling for performance/profiling
  • PII Protection: send_default_pii=False
sentry_sdk.init(
    dsn=settings.sentry_dsn,
    integrations=[
        StarletteIntegration(transaction_style="endpoint"),
        FastApiIntegration(transaction_style="endpoint"),
    ],
    traces_sample_rate=0.1,
    profiles_sample_rate=0.1,
    environment=settings.environment,
    send_default_pii=False,  # Privacy protection
)

10. Correlation IDs / Request Tracing

Status: PASS

  • Location: app/main.py:311-345
  • Features:
  • Auto-generated UUID for each request
  • Accepts X-Request-ID header for distributed tracing
  • Stored in request.state.correlation_id
  • Returned in X-Request-ID response header
  • Included in structured log messages
correlation_id = request.headers.get("X-Request-ID") or str(uuid.uuid4())
request.state.correlation_id = correlation_id

11. Additional Security Features

Content Moderation

  • Status: Planned (Phase 9)
  • Current: Basic keyword filtering

Password/Secret Exposure

  • Status: PASS
  • No passwords stored (phone OTP authentication only)
  • Secrets loaded from environment variables
  • Production validators prevent default secrets

HTTPS

  • Status: PASS
  • Railway deployment enforces HTTPS
  • Secure cookies enabled in production

Summary

Category Status Notes
Rate Limiting PASS Redis-backed, all public endpoints covered
Input Validation PASS Pydantic models throughout
SQL Injection PASS SQLAlchemy ORM only
JWT Security PASS Proper expiry, algorithm, validation
OAuth Token Encryption PASS Fernet encryption
Webhook Signatures PASS Stripe + Twilio verified
CORS PASS Environment-specific origins
CSRF PASS Selective middleware
Log Sanitization PASS* Minor phone logging improvements recommended
Sentry PASS Full integration with PII protection
Correlation IDs PASS Full request tracing

Action Items (Non-blocking)

  1. Add rate limiting to edge agent routes
  2. Improve phone number masking in OAuth logs
  3. Consider per-user rate limiting for authenticated endpoints

Last Updated: January 10, 2026