Security Architecture
Ampra implements defense-in-depth security across every layer — from network transport to application logic — following industry best practices for multi-tenant SaaS platforms.
Transport Security
| Layer | Mechanism |
|---|---|
| Client ↔ API | TLS 1.3 via Traefik with automatically-provisioned Let's Encrypt certificates |
| Client ↔ MQTT | MQTT over TCP with per-client API key authentication |
| Internal Services | Docker bridge network isolation — no external ports exposed for inter-service communication |
| EMQX Dashboard | Exposed via Traefik with TLS at emqx.ampra.solar |
| MinIO Object Storage | Presigned URLs with 7-day expiry for asset retrieval; TLS on public endpoint |
Authentication
Cookie-Based Session Authentication
Ampra uses ASP.NET Identity with cookie-based sessions rather than JWT tokens:
| Property | Value |
|---|---|
HttpOnly | true — prevents JavaScript access to session cookies |
SecurePolicy | Always — cookies only sent over HTTPS |
SameSite | Lax — prevents CSRF while allowing top-level navigation |
ExpireTimeSpan | 7 days with sliding expiration |
Password Policy
| Rule | Requirement |
|---|---|
| Minimum length | 8 characters |
| Uppercase | At least one uppercase letter |
| Lowercase | At least one lowercase letter |
| Digit | At least one numeric digit |
| Special character | Not required |
Account Lockout
After 5 consecutive failed login attempts, the account is locked for 5 minutes. This is enforced at the Identity framework level.
Email Verification
- 6-digit verification codes sent via SendGrid
- Codes expire after 10 minutes
- After 5 failed verification attempts, the account is locked from verification for 15 minutes
- Rate limiting on code resend requests (minimum 60-second interval)
Authorization
Role-Based Access Control (RBAC)
| Constraint | Enforcement |
|---|---|
| Overseer accounts cannot be deleted | Checked in AdminService.DeleteUserAsync |
| Non-Overseers cannot assign Overseer role | Checked in AdminService.UpdateUserAsync |
| Users cannot modify their own role | Self-modification prevention in admin endpoints |
| Debug tools restricted to Admin+ | [Authorize(Roles = "Overseer,Admin")] on debug endpoints |
Resource Ownership Validation
Every API call that accesses user-scoped data passes through IOwnershipService:
// Pattern used across all controllers
var userId = GetUserId();
await _ownershipService.ValidateSourceOwnershipAsync(sunSourceId, userId);
This throws UnauthorizedAccessException (mapped to HTTP 403) if the requesting user does not own the target sun source. The ownership check queries PostgreSQL with AsNoTracking() for performance.
MQTT Security
Client Authentication Flow
ACL (Access Control List) Validation
Security Measures
| Measure | Implementation |
|---|---|
| API Key Generation | 64-character cryptographic hex string (RandomNumberGenerator) |
| Internal API Key | Constant-time comparison via CryptographicOperations.FixedTimeEquals |
| Topic Isolation | Each source can only publish to ampra/sources/{sourceId}/data |
| Super-User | The ingestion worker authenticates as a super-user with subscription privileges |
Webhook Security
Webhook payloads are authenticated using HMAC-SHA256:
| Step | Detail |
|---|---|
| 1. Secret Generation | 32-byte secret generated via RandomNumberGenerator, Base64-encoded |
| 2. Signing | Device computes HMAC-SHA256(body, secret) and sends in X-Webhook-Secret header |
| 3. Validation | API computes the same HMAC and uses constant-time comparison to prevent timing attacks |
| 4. Rotation | Users can rotate secrets via POST /api/sunsources/{id}/rotate-secret |
Input Validation
All incoming requests are validated through FluentValidation rules applied as a global action filter:
| Validation Layer | Mechanism |
|---|---|
| Request DTOs | FluentValidation rules (10 validators covering all input models) |
| Global Filter | ValidationFilter intercepts all controller actions, validates arguments before execution |
| Range Clamping | API parameters like page, pageSize, limit are clamped to safe ranges |
| File Upload | Magic byte validation (not just MIME type) for image uploads, 5MB size limit |
| Request Size | Webhook endpoint limited to 65KB via [RequestSizeLimit(65_536)] |
Error Handling
The GlobalExceptionMiddleware centralizes all error responses:
| Exception Type | HTTP Status | Response |
|---|---|---|
UnauthorizedAccessException | 403 Forbidden | {"message": "Access denied"} |
NotFoundException | 404 Not Found | {"message": "<detail>"} |
ArgumentException | 400 Bad Request | {"message": "Invalid request"} |
InvalidOperationException | 400 Bad Request | {"message": "This operation could not be completed"} |
| Unhandled exceptions | 500 Internal Server Error | {"message": "An unexpected error occurred"} |
Internal exception details are never exposed to the client. All unhandled exceptions are logged server-side with full context (method, path, stack trace).
Data Protection
| Concern | Implementation |
|---|---|
| Passwords | Hashed via ASP.NET Identity (PBKDF2 with HMAC-SHA256, 100K iterations) |
| MQTT API Keys | Stored as plain hex in PostgreSQL (they are machine-generated secrets, not user passwords) |
| Webhook Secrets | Stored as Base64-encoded byte array in PostgreSQL |
| Upload Isolation | Files stored under {userId}/ prefix in MinIO — users can only delete their own files |
| Presigned URLs | MinIO asset URLs expire after 7 days, preventing permanent link sharing |