Security Hardening
Guida alla messa in sicurezza di un’applicazione costruita con Pragmatic.Design. Copre il modello di sicurezza di ogni modulo rilevante e le azioni che spettano al consumer.
Per segnalare una vulnerabilità: vedi SECURITY.md. Non aprire issue pubbliche.
Principio: fail-closed
Section titled “Principio: fail-closed”I moduli Pragmatic adottano default fail-closed dove la sicurezza è in gioco:
Authorization— laDefaultEndpointPolicya runtime èRequireAuthenticated; un endpoint senza policy esplicita richiede autenticazione.Actions— la pipeline dei filtri (Validation → Permission → Policy → Resource) nega se un authorizer non concede;AnonymousUserhaNullUserAuthorization(deny-all).Email— l’invio rifiuta di autenticarsi seUseSslè richiesto ma TLS non è stato stabilito.Migrations— le breaking change (DROP) sono bloccate salvoForce=trueesplicito.
Identity & autenticazione
Section titled “Identity & autenticazione”JWT (Pragmatic.Identity.Local.Jwt)
Section titled “JWT (Pragmatic.Identity.Local.Jwt)”- Signing key: minimo 32 byte (256 bit) per HMAC-SHA256 — validato a
UseJwtAuthentication. Genera una chiave casuale, non una passphrase. - Issuer/Audience: in ambiente Production devono essere entrambi configurati — altrimenti
UseJwtAuthenticationlancia. Senza, il validatore accetterebbe token di qualunque issuer/audience. - Algoritmo: HMAC-SHA256 fisso, nessun supporto per
alg: none. - Segreti: la signing key va da un secret manager (Key Vault, env var, user-secrets), mai hardcoded né in
appsettings.jsoncommittato. - Revoca: i token JWT sono stateless — non sono revocabili prima della scadenza. Tieni
TokenExpirationbreve (default 1h). Per revoca immediata serve un blocklist store esterno (lato consumer).
Login locale (Pragmatic.Identity.Local)
Section titled “Login locale (Pragmatic.Identity.Local)”- Lockout per-account: attivo via
MaxFailedLoginAttempts+LockoutDurationinLocalIdentityOptions. Configura valori adatti al tuo rischio. - Rate-limit IP-level: non incluso di default. Per difesa in profondità contro spraying volumetrico, monta
AddRateLimiter/UseRateLimiterdi ASP.NET Core sull’endpoint di login. - Handler di sviluppo:
NoOpAuthenticationHandlereHeaderUserMiddlewaresono solo per sviluppo. Verifica che la configurazione di Production usiUseJwtAuthentication.
Authorization
Section titled “Authorization”- La cache dei permessi (
UsePermissionCache) ha TTL: una modifica di ruolo/permesso a runtime diventa effettiva al più tardi dopo il TTL. Per propagazione immediata, riduci il TTL o invalida esplicitamente. - Le
internal callnella pipeline Actions bypassano il controllo permessi (sono in-process, stesso trust boundary) e non lasciano audit trail: tienine conto nel modello di audit.
Persistence
Section titled “Persistence”- Visibilità dei dati: usa
[OwnedEntity]/[ScopedEntity]invece di filtri manuali — il SG genera i query filter e i bypass via permission. [WithoutFilter]: disabilita i query filter per un tipo. È un privilegio, non un escape hatch: applicalo solo a query in contesti già autorizzati (admin/background), mai per aggirare un filtro inadeguato.- Connection string: dal secret manager, mai loggate. Pragmatic non le logga.
Storage file (Pragmatic.Storage)
Section titled “Storage file (Pragmatic.Storage)”LocalDiskFileStorage è inteso per sviluppo/demo. In ogni caso:
- Path traversal: il parametro
containerè validato — un valore che risolve fuori da{basePath}/files/viene rifiutato. Anche le letture (GetAsync/DeleteAsync) restano confinate alla root. - Limite dimensione: passa
maxFileSizeBytesal costruttore per rifiutare upload oltre soglia (vale anche per stream non seekable). Default0= illimitato — imposta un limite in produzione. - Content-type / estensione: la validazione del tipo di file caricato è responsabilità del consumer. Non fidarti del content-type dichiarato dal client.
- In produzione usa un backend dedicato (Azure Blob, S3) servito dietro CDN, non il disco locale dell’host.
Email (Pragmatic.Email)
Section titled “Email (Pragmatic.Email)”- TLS: con
UseSsl = truela connessione rifiuta di autenticarsi se STARTTLS non è disponibile — le credenziali non viaggiano mai in chiaro. TieniUseSsl = truein produzione. - Header injection: i valori degli header (subject, display name, header custom, nome allegato) sono sanificati/codificati — un
\r\nnon può iniettare header. I nomi di header custom non validi vengono rifiutati. - Credenziali SMTP: dal secret manager.
Messaging
Section titled “Messaging”- Credenziali transport: la connection string RabbitMQ non viene loggata in chiaro (solo
host:port/vhost). Tienila comunque in un secret manager. - Outbox: la consegna è at-least-once — un consumer può ricevere lo stesso messaggio più volte. Gli handler devono essere idempotenti (es. dedup su
MessageId). - Deserializzazione: i tipi dei messaggi sono risolti via registry generato dal SG (FQN → Type), non via
Type.GetType()su input arbitrario.
Logging
Section titled “Logging”- Log forging: i valori che finiscono nel messaggio di log hanno CR/LF neutralizzati — un input utente non può iniettare righe di log false.
- Dati sensibili: non loggare password, token, PII. Usa
[NotLogged]sui campi sensibili dei tipi che vengono loggati strutturati.
Gateway (Pragmatic.Gateway)
Section titled “Gateway (Pragmatic.Gateway)”- CORS: la combinazione
Origins: ["*"]+AllowCredentials: trueè invalida e viene rifiutata all’avvio con un messaggio chiaro. Per le credenziali, elenca origin espliciti. - Termina il TLS al gateway; le rotte interne presuppongono rete fidata.
ControlPlane / Agent
Section titled “ControlPlane / Agent”- Agent socket (Unix): il socket è
0600e la sua directory0700— il confine di trust è l’uid del processo agent. Non allentare questi permessi. - ControlPlane hub: i payload dei comandi sono validati (non vuoti, dimensione limitata, JSON ben formato). L’accesso resta protetto da API key — esponilo solo su rete interna e sempre via HTTPS.
Remote boundaries (/_pragmatic/invoke)
Section titled “Remote boundaries (/_pragmatic/invoke)”L’endpoint generato POST /_pragmatic/invoke è RPC interno boundary-to-boundary. È fail-closed di default: senza configurazione richiede un principal autenticato. Il client remoto generato propaga l’identità del chiamante inoltrando l’header Authorization sulla chiamata in uscita (handler PragmaticRemoteAuthHandler registrato su ogni named client).
- Default (raccomandato): nessuna configurazione → l’endpoint richiede autenticazione, l’identità del chiamante viene propagata. Una chiamata senza credenziale viene rifiutata.
- Policy di servizio: imposta
Pragmatic:RemoteBoundaries:InvokeEndpoint:AuthorizationPolicycon il nome di una policy registrata per un controllo service-to-service più stringente. - Opt-out rete fidata:
Pragmatic:RemoteBoundaries:InvokeEndpoint:AllowAnonymous: truerende l’endpoint anonimo — usalo solo su rete isolata. Non è mai silenziosamente anonimo: l’opt-out è esplicito e tracciabile in configurazione.
⚠️ Se i tuoi host comunicano da contesti senza HTTP context (job/background), non c’è un header da propagare: configura una credenziale di servizio esplicita oppure
AllowAnonymoussu rete fidata.
Checklist pre-produzione
Section titled “Checklist pre-produzione”- JWT: signing key ≥256 bit da secret manager; Issuer + Audience configurati;
TokenExpirationbreve. - Autenticazione di Production = JWT (non
NoOp/header). - Lockout login configurato; rate-limit IP sull’endpoint di login.
-
Storage: limite dimensione upload impostato; backend di produzione (non disco locale). -
Email:UseSsl = true; credenziali da secret manager. - Messaging: handler idempotenti (outbox è at-least-once).
- Connection string e segreti fuori dai file di configurazione committati.
- CORS: origin espliciti se servono credenziali.
- HTTPS ovunque; ControlPlane/Agent solo su rete interna.
- Remote invoke (
/_pragmatic/invoke): lasciato fail-closed, oppureAllowAnonymous: truesolo su rete isolata e documentato. - Migrations: nessun
Force=truedi default; backup prima di ogni migrazione distruttiva.