Ho costruito un Third Party Management (TPM) tool conforme al DORA: un’applicazione Django + React per gestire i cicli di vita dei fornitori ICT, le valutazioni del rischio e i registri regolatori. Ho poi integrato il SSO Microsoft Entra ID, containerizzato il tutto e deployato su AWS ECS Fargate con una pipeline Terraform, in un ambiente aziendale pieno di proxy, permission boundary e infrastruttura condivisa.
Questo articolo copre l’intero percorso dal primo commit a due task di produzione in salute, incluso ogni errore e ogni correzione.
Cosa fa l’applicazione#
Il tool TPM gestisce il ciclo di vita completo dei fornitori ICT di terze parti nell’ambito della regolamentazione DORA. Il flusso principale è una state machine a cinque fasi per ogni fornitore:
flowchart LR
ID[Identification] --> DD[Due Diligence]
DD --> AG[Agreement]
AG --> MO[Monitoring]
MO --> EX[Exit]
Ogni caso traccia valutazioni del rischio, accordi contrattuali, report SLA e genera i registri regolatori RT (RT.01.01, RT.02.01, ecc.) che le banche devono trasmettere alle autorità di vigilanza. L’app comprende 15 app Django, oltre 30 endpoint API, un motore di questionari dinamici, template di clausole contrattuali e un portale separato per i fornitori.
Lo stack:
- Backend: Django 5.1 / DRF / PostgreSQL 16 / SimpleJWT
- Frontend: React 18 / TypeScript / Vite / shadcn/ui
- Auth: Microsoft Entra ID SSO (OAuth2 + PKCE)
- Infra: ECS Fargate / Aurora Serverless v2 / ALB / Terraform
Aggiungere il SSO senza MSAL#
Il primo compito era aggiungere il SSO Microsoft Entra ID. Il requisito era chiaro: nessuna libreria MSAL, nessuna sessione lato server. Flusso OAuth2 Authorization Code puro con PKCE, implementato da zero.
Il flusso è il seguente:
sequenceDiagram
participant Browser
participant Backend
participant Microsoft
participant Graph
Browser->>Backend: GET /api/auth/sso/config/
Backend-->>Browser: {client_id, authority, redirect_uri}
Browser->>Browser: Genera PKCE verifier + challenge
Browser->>Microsoft: Redirect a /authorize?code_challenge=...
Microsoft-->>Browser: Redirect a /auth/callback?code=...
Browser->>Backend: POST /api/auth/sso/callback/ {code, code_verifier}
Backend->>Microsoft: Scambia il codice con i token
Microsoft-->>Backend: {id_token, access_token}
Backend->>Backend: Valida id_token via JWKS
Backend->>Graph: GET /me/photo/$value
Graph-->>Backend: Bytes JPEG
Backend->>Backend: Salva foto come data URI base64 nel DB
Backend-->>Browser: Token JWT {access, refresh}
Lato backend, valido l’ID token recuperando le chiavi JWKS di Microsoft (con cache di un’ora) e verificando la firma RS256 con PyJWT:
def _validate_id_token(id_token: str) -> dict[str, Any]:
header = jwt.get_unverified_header(id_token)
keys = _get_jwks_keys()
matching = [k for k in keys if k.get("kid") == header.get("kid")]
if not matching:
raise jwt.InvalidTokenError(f"No matching key for kid={header.get('kid')}")
public_key = jwt.algorithms.RSAAlgorithm.from_jwk(matching[0])
return jwt.decode(
id_token,
key=public_key,
algorithms=["RS256"],
audience=settings.ENTRA_CLIENT_ID,
issuer=settings.ENTRA_ISSUER,
)Lato frontend, il PKCE è implementato usando la Web Crypto API senza dipendenze esterne:
export function generateCodeVerifier(): string {
const array = new Uint8Array(64);
crypto.getRandomValues(array);
return base64urlEncode(array.buffer);
}
export async function generateCodeChallenge(verifier: string): Promise<string> {
const data = new TextEncoder().encode(verifier);
const digest = await crypto.subtle.digest('SHA-256', data);
return base64urlEncode(digest);
}Un bug sottile che ho incontrato: lo StrictMode di React 18 esegue gli effect due volte in sviluppo. La pagina di callback OAuth puliva sessionStorage al primo run, quindi il secondo run non trovava il PKCE verifier e mostrava brevemente un errore “Accesso fallito” prima che la navigazione andasse a buon fine. La correzione è un guard con useRef:
const startedRef = useRef(false);
useEffect(() => {
if (startedRef.current) return;
startedRef.current = true;
// ... scambia il codice, naviga
}, []);Le foto profilo da Microsoft Graph vengono recuperate durante il callback SSO usando l’access_token, poi salvate come data URI base64 in un TextField sul modello User. Questo evita la necessità di storage persistente per i file (importante su Fargate dove i container sono effimeri).
Migrazione da CloudFormation a Terraform#
L’infrastruttura originale usava CloudFormation. Dopo una serie di fallimenti dolorosi con i change set di CloudFormation (stack bloccato in ROLLBACK_FAILED, subnet group Aurora che bloccava l’eliminazione, ForceNewDeployment non valido in fase CREATE), ho migrato tutto a Terraform, prendendo come riferimento un progetto interno.
La struttura Terraform:
infrastructure/terraform/
provider.tf # Provider AWS, backend S3
variables.tf # Variabili di input
locals.tf # Convenzioni di naming, subnet per ambiente
main.tf # ECS, ALB, IAM, Secrets Manager
aurora.tf # Aurora Serverless v2
outputs.tf # URL, endpoint del cluster
backends/
qual.hcl
prod.hclUna scelta di design importante: Aurora usa manage_master_user_password = true, quindi RDS gestisce la rotazione delle credenziali automaticamente e salva il segreto JSON in Secrets Manager. ECS estrae i singoli campi usando la sintassi con chiave JSON:
secrets = [
{
name = "DB_USERNAME"
valueFrom = "${aws_rds_cluster.aurora[0].master_user_secret[0].secret_arn}:username::"
},
{
name = "DB_PASSWORD"
valueFrom = "${aws_rds_cluster.aurora[0].master_user_secret[0].secret_arn}:password::"
}
]L’URL del registry ECR era inizialmente hardcoded sull’account ID dell’ambiente qual. Questo causava il fallimento dei container prod nel pull delle immagini con un 403. La correzione è stata usare data.aws_caller_identity.current.account_id in modo dinamico:
registry = "${data.aws_caller_identity.current.account_id}.dkr.ecr.${var.aws_default_region}.amazonaws.com"La Pipeline: 15 Fix Prima del Verde#
Far funzionare la pipeline GitLab CI/CD in un ambiente aziendale ha richiesto più iterazioni del previsto. Ecco l’elenco completo dei fallimenti e delle correzioni, in ordine:
Job SonarQube in pending: nessun runner con tag 'mgm'
mgm per SonarQube. I nostri runner erano taggati mgm-qual e mgm-prod. Correzione: aggiunto il tag mgm al runner prod via API GitLab.Docker build fallito: errore uv sync --no-editable
uv sync --frozen --no-dev --no-editable falliva perché hatchling non riusciva a trovare il pacchetto del progetto. Correzione: passato a --no-install-project (installa solo le dipendenze, non il progetto stesso).Bug nel rilevamento CS_TYPE di CloudFormation
CS_TYPE=$(aws describe-stacks ... && echo UPDATE || echo CREATE). Quando lo stack esisteva, questo produceva DELETE_COMPLETE\nUPDATE (due righe), causando il fallimento di create-change-set. Correzione: parsing corretto della stringa di stato.ECS service CREATE_FAILED: validazione del modello
Weight, DesiredCount, ecc. come stringhe. Inoltre, ForceNewDeployment non è valido in fase di CREATE iniziale. Correzione: rimosso ForceNewDeployment. La migrazione a Terraform ha eliminato anche questi problemi di coercizione di tipo.Provider Terraform falliti: registry.terraform.io irraggiungibile
before_script del template CI base imposta HTTP_PROXY, ma il nostro before_script a livello di job lo sovrascrive. Correzione: export esplicito delle variabili proxy prima di terraform init.apk add fallito: l'immagine Docker usa Debian, non Alpine
minion-base-aws) è basata su Debian. La pipeline tentava apk add docker-cli-buildx, che falliva. Correzione: rimossa la riga apk add (il runner ha già gli strumenti necessari).Docker build: no space left on device
terraform-modules/ clonato dal before_script del template base. Correzione: aggiunto .dockerignore, usato docker system prune -af prima della build, EBS espanso a 50GB via gosp-cloud CLI.npm install: errore ERESOLVE
package-lock.json e rieseguiva npm install da zero, incappando in conflitti di peer dependency. Correzione: sostituito con npm ci (usa il lockfile).apt-get fallisce dentro Docker: impossibile raggiungere deb.debian.org
HTTP_PROXY e HTTPS_PROXY di Docker sono impostati, ma apt non sempre rispetta le variabili d’ambiente della shell. Correzione: scrivere esplicitamente un file di configurazione proxy per apt prima di apt-get update.Errore di permessi su staticfiles in ECS
collectstatic falliva con PermissionError: /app/backend/staticfiles/admin. Il Dockerfile creava la directory come root prima di passare a appuser. Correzione: aggiunto RUN chown -R appuser:appuser /app dopo tutte le copie.L'health check ALB restituisce 400
Host. Django lo rifiuta via ALLOWED_HOSTS. Correzione: nginx passa Host: localhost (non $host) specificatamente per l’endpoint /api/health/.Mixed content: avatar servito in HTTP
http:// per i media file perché l’ALB termina il TLS e passa semplice HTTP a nginx. Correzione: aggiunto SECURE_PROXY_SSL_HEADER = ('HTTP_X_FORWARDED_PROTO', 'https') nelle impostazioni di produzione, e nginx ora passa il X-Forwarded-Proto originale dall’ALB.Avatar 404 dopo il deploy
/media/ come prefisso. Il campo ImageField era stato cambiato in TextField nel modello, ma il serializer lo rilevava diversamente. Correzione: dichiarare esplicitamente avatar = serializers.CharField(read_only=True, default=None) nel serializer.ECR 403 su ECS prod
data.aws_caller_identity.current.account_id dinamicamente nei locals.seed_data AttributeError: TPMCase non ha l'attributo title
case.title ma il campo si chiama case.case_number. Correzione: grep-and-replace in quattro righe in notification_generator.py.Protezione dei branch e flusso di rilascio#
Una volta che tutto funzionava, ho configurato la protezione dei branch per applicare un flusso di promozione corretto:
flowchart LR
main["main\n(Developers+)"] -->|MR| prerelease["pre-release\n(Maintainers)"]
prerelease -->|MR| release["release\n(Nessun push diretto)"]
main -->|CI trigger| qual[ambiente qual]
release -->|CI trigger| prod[ambiente prod]
Il branch release ha push_access_level = 0 (nessuno può fare push direttamente). Ogni deploy in produzione richiede una merge request attraverso pre-release. Lo step di apply in entrambe le pipeline è manuale, fornendo al team un gate di revisione prima che le modifiche all’infrastruttura arrivino in produzione.
I numeri#
Al termine della sessione:
- 5.758 righe aggiunte, 831 rimosse in 16 commit (solo il lavoro su infrastruttura e SSO)
- 526 test backend che passano
- SonarQube: 0 bug o vulnerabilità aperti (7 corretti, 158 hotspot di password di test marcati come sicuri)
- qual deployato: 2/2 task ECS in esecuzione, database popolato con seed data
- prod deployato: 2/2 task ECS in esecuzione
Il tempo reale è stato di poco meno di 9 ore. Due ore di quelle erano di attesa per il provisioning di Aurora Serverless v2 (circa 7 minuti per stack, su diversi deploy falliti e ripetuti).
Lezioni apprese#
Alcune cose che avrebbero risparmiato molto tempo:
Il lavoro di conformità DORA è pesante sul fronte infrastrutturale. Le viste del registro regolatorio, i calcoli del rischio e la state machine del ciclo di vita sono complessi, ma la pipeline di deploy ha causato più attrito totale rispetto alla logica applicativa.
Terraform batte CloudFormation per lo sviluppo iterativo. L’output di terraform plan è più chiaro, la gestione dello stato è più trasparente, e il meccanismo lifecycle { ignore_changes = [...] } ha salvato diverse ore nella gestione delle password Aurora.
Proxy aziendale + Docker: controlla sempre il Dockerfile. Ogni RUN che scarica qualcosa (apt, pip, npm) ha bisogno di awareness del proxy. Scrivere $HTTP_PROXY come ARG/ENV non è sempre sufficiente. Configurare esplicitamente apt (/etc/apt/apt.conf.d/proxy.conf) è più sicuro.
I container Fargate sono stateless by design. Salvare le foto avatar come data URI base64 nel database era la scelta giusta per questa scala. Per file più grandi o traffico elevato, un bucket S3 per i media è la soluzione corretta, ma aggiunge complessità operativa.
Abilitare ECS Exec fin dal primo giorno vale la pena. Eseguire manage.py seed_data ha richiesto il lancio di un task one-off con un override di comando complesso. Con ECS Exec abilitato, sarebbe stata una singola chiamata aws ecs execute-command.
Se stai costruendo strumenti di conformità DORA o configurando ECS Fargate con Terraform in un ambiente aziendale, scrivimi a manuel.fedele+website@gmail.com.