Skip to main content

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

LayerMechanism
Client ↔ APITLS 1.3 via Traefik with automatically-provisioned Let's Encrypt certificates
Client ↔ MQTTMQTT over TCP with per-client API key authentication
Internal ServicesDocker bridge network isolation — no external ports exposed for inter-service communication
EMQX DashboardExposed via Traefik with TLS at emqx.ampra.solar
MinIO Object StoragePresigned URLs with 7-day expiry for asset retrieval; TLS on public endpoint

Authentication

Ampra uses ASP.NET Identity with cookie-based sessions rather than JWT tokens:

PropertyValue
HttpOnlytrue — prevents JavaScript access to session cookies
SecurePolicyAlways — cookies only sent over HTTPS
SameSiteLax — prevents CSRF while allowing top-level navigation
ExpireTimeSpan7 days with sliding expiration

Password Policy

RuleRequirement
Minimum length8 characters
UppercaseAt least one uppercase letter
LowercaseAt least one lowercase letter
DigitAt least one numeric digit
Special characterNot 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)

ConstraintEnforcement
Overseer accounts cannot be deletedChecked in AdminService.DeleteUserAsync
Non-Overseers cannot assign Overseer roleChecked in AdminService.UpdateUserAsync
Users cannot modify their own roleSelf-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

MeasureImplementation
API Key Generation64-character cryptographic hex string (RandomNumberGenerator)
Internal API KeyConstant-time comparison via CryptographicOperations.FixedTimeEquals
Topic IsolationEach source can only publish to ampra/sources/{sourceId}/data
Super-UserThe ingestion worker authenticates as a super-user with subscription privileges

Webhook Security

Webhook payloads are authenticated using HMAC-SHA256:

StepDetail
1. Secret Generation32-byte secret generated via RandomNumberGenerator, Base64-encoded
2. SigningDevice computes HMAC-SHA256(body, secret) and sends in X-Webhook-Secret header
3. ValidationAPI computes the same HMAC and uses constant-time comparison to prevent timing attacks
4. RotationUsers 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 LayerMechanism
Request DTOsFluentValidation rules (10 validators covering all input models)
Global FilterValidationFilter intercepts all controller actions, validates arguments before execution
Range ClampingAPI parameters like page, pageSize, limit are clamped to safe ranges
File UploadMagic byte validation (not just MIME type) for image uploads, 5MB size limit
Request SizeWebhook endpoint limited to 65KB via [RequestSizeLimit(65_536)]

Error Handling

The GlobalExceptionMiddleware centralizes all error responses:

Exception TypeHTTP StatusResponse
UnauthorizedAccessException403 Forbidden{"message": "Access denied"}
NotFoundException404 Not Found{"message": "<detail>"}
ArgumentException400 Bad Request{"message": "Invalid request"}
InvalidOperationException400 Bad Request{"message": "This operation could not be completed"}
Unhandled exceptions500 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

ConcernImplementation
PasswordsHashed via ASP.NET Identity (PBKDF2 with HMAC-SHA256, 100K iterations)
MQTT API KeysStored as plain hex in PostgreSQL (they are machine-generated secrets, not user passwords)
Webhook SecretsStored as Base64-encoded byte array in PostgreSQL
Upload IsolationFiles stored under {userId}/ prefix in MinIO — users can only delete their own files
Presigned URLsMinIO asset URLs expire after 7 days, preventing permanent link sharing