Zum Inhalt springen

Architecture overview

Dieser Inhalt ist noch nicht in deiner Sprache verfügbar.

Pia Server is an ASP.NET Core 10 Minimal-API service. There are no MVC controllers — endpoints are grouped into modules (auth, sync, AI proxy, devices, recovery) and each module exposes a MapXxxEndpoints() extension method that Program.cs calls.

ModuleFilePurpose
AuthEndpointsAuth/AuthEndpoints.csOAuth login, refresh, logout, /auth/me
SyncEndpointsSync/SyncEndpoints.csCursor-based pull, conflict-aware push, status, audit, quota
AiProxyEndpointsAiProxy/AiProxyEndpoints.csForward client requests to upstream AI providers
DeviceEndpointsE2EE/DeviceEndpoints.csRegister / approve / revoke E2EE devices, exchange wrapped UMK
RecoveryEndpointsE2EE/RecoveryEndpoints.csStore / retrieve recovery-wrapped UMK, activate via recovery code

Health check at GET /health.

UseHsts → UseHttpsRedirection → UseAuthentication → UseAuthorization → UseRateLimiter → Endpoints

The DI container in Program.cs registers:

ServiceLifetimeNotes
PiaDbContextScopedEF Core, provider chosen by config
JwtServiceSingletonHS256, issuer pia-server, audience pia-client
UserServiceScopedUser CRUD, find-or-create on OAuth callback
EncryptionServiceSingletonAES-256-GCM with per-user keys via HKDF
AiProxyServiceScopedRoutes per-mode AI requests to upstream providers
ConflictResolverScopedLast-write-wins sync conflict resolution
QuotaServiceScopedEnforces per-user object caps before push
SyncServiceScopedSync orchestration
OnboardingSessionServiceSingletonIn-memory E2EE pairing sessions (10-min expiry)
RefreshTokenCleanupServiceHostedBackground prune of expired refresh tokens

OnboardingSessionService is in-memory by design — pairing sessions are short-lived and don’t need to survive a restart. If you run the server in a load-balanced deployment, route all /api/e2ee/* calls for a given device pairing to the same node, or replace the implementation with a Redis-backed one.

A typical authenticated sync request looks like this:

  1. Caddy terminates TLS and forwards to pia-server.
  2. The rate limiter applies the sync policy (per-user budget).
  3. UseAuthentication validates the JWT and populates HttpContext.User.
  4. The endpoint handler receives the SyncPushRequest body.
  5. QuotaService.CheckQuotaAsync rejects with 409 quota_exceeded if any cap would be violated.
  6. ConflictResolver reconciles per-record UpdatedAt timestamps.
  7. EF Core writes the changes inside a single transaction.
  8. The endpoint returns a SyncPushResponse with server-assigned IDs and any conflict metadata.

E2EE traffic adds a parallel side-channel: the server stores wrapped UMK material via DeviceEndpoints/RecoveryEndpoints, but never sees plaintext keys.

Configuration is layered (appsettings.jsonappsettings.{Environment}.json → environment variables → command-line args). The key sections are:

SectionPurpose
DatabaseProvider (sqlite / postgresql) and connection string
JwtIssuer, audience, signing key, lifetimes
OAuthPer-provider client IDs and secrets
AiDefault provider + per-mode overrides
EncryptionMaster key (must come from env in production)
LicenseLicence file path

See Configuration for the full reference.

A few non-obvious hardening choices are worth knowing about so you don’t accidentally undo them in a refactor:

  • Email enumeration: /auth/login/local is timing-padded (300 ms floor) and runs a dummy hash on the unknown-email branch. The error body is identical for “unknown email” and “wrong password”. Don’t introduce branches that return earlier than the floor.
  • /auth/forgot-password always returns 200, regardless of whether the email is registered. Reset tokens are minted only for real users; the response is identical either way.
  • /auth/register intentionally returns 409 email_exists for duplicates (UX trade-off accepted), with rate limiting in front.
  • OAuth callback collisions: when an OAuth provider brings an email that already exists under a different provider, the server returns 409 email_exists — the same response as /auth/register. This leaks existence but cannot be probed anonymously.