Lab 45 – Autenticazione a due fattori: TOTP, WebAuthn, codici di recupero
Dipendenze
Sezione intitolata “Dipendenze”- Lab precedenti:
- Sistemi: nodi
desopve01-03; un PC di amministrazione con browser, un’app TOTP (sul telefono o un programma comeoathtool) e una chiave di sicurezza FIDO2 (o un autenticatore di piattaforma, per esempio Windows Hello). - Credenziali:
root@pam,stagista@pve,mario.rossi, tutti con<password-lab>→ Appendice B.
Obiettivi
Sezione intitolata “Obiettivi”- Attivare il TOTP per un utente ed entrare con il secondo fattore.
- Configurare WebAuthn per tutto il cluster e registrare una chiave di sicurezza FIDO2.
- Generare e usare i codici di recupero.
- Vedere il blocco dopo troppi codici sbagliati e sbloccare l’utente.
- Rendere la 2FA obbligatoria per un intero realm.
- Capire cosa cambia per le API, per i token e per root@pam.
Come funziona
Sezione intitolata “Come funziona”Con la 2FA il login richiede due prove: la password (qualcosa che sai) e un secondo fattore (qualcosa che hai). Proxmox VE ne supporta quattro tipi, configurabili insieme sullo stesso utente:
| Tipo | Cos’è | Note |
|---|---|---|
| TOTP | codice di 6 cifre che cambia ogni 30 secondi, generato da un’app | il segreto si condivide una volta, con un QR code |
| WebAuthn | chiave di sicurezza FIDO2 (USB/NFC) o autenticatore della piattaforma | resiste al phishing: la firma è legata al dominio |
| Recovery keys | 10 codici monouso | per quando si perde il telefono o la chiave |
| Yubico OTP | codici delle chiavi YubiKey verificati da un server Yubico | richiede un server di validazione |
I fattori di ogni utente sono in /etc/pve/priv/tfa.cfg, condiviso dal cluster. Un utente li gestisce da sé dal menu utente → TFA; un amministratore da Datacenter → Permissions → Two Factor.
La 2FA protegge il login con password (GUI e API /access/ticket). Gli API token e l’accesso SSH/console del nodo non ne sono toccati.
WebAuthn e il dominio. Il browser firma la sfida solo se l’indirizzo del sito corrisponde all’ID configurato (il dominio, qui proxmox.community) e solo su una connessione con certificato valido: con l’avviso “certificato non attendibile” WebAuthn non funziona.
Procedura
Sezione intitolata “Procedura”1. TOTP per un utente
Sezione intitolata “1. TOTP per un utente”In Datacenter → Permissions → Two Factor → Add → TOTP si sceglie l’utente stagista@pve e si dà una descrizione. Proxmox VE genera un segreto e il QR code: si inquadra con l’app (o si copia il segreto) e si inserisce il primo codice in Verify Code, che dimostra che l’app è configurata bene.

Nel lab il codice viene da oathtool, che fa lo stesso calcolo di un’app TOTP:
oathtool --totp -b <segreto-base32>268992Dopo la password, il login chiede il secondo fattore:

2. WebAuthn per il cluster
Sezione intitolata “2. WebAuthn per il cluster”WebAuthn va configurato una volta per tutto il datacenter in Datacenter → Options → WebAuthn Settings: Name (il nome mostrato dal browser), Origin (l’indirizzo da cui si usa la GUI) e ID (il dominio). Auto-fill li compila con l’indirizzo corrente.

Prima prova: origin di un solo nodo. Configuriamo l’origin di desopve01 e il dominio come ID:
pvesh set /cluster/options --webauthn "rp=Proxmox Community Lab,origin=https://desopve01.proxmox.community:8006,id=proxmox.community,allow-subdomains=1"Da https://desopve01.proxmox.community:8006 il login con la chiave funziona; da https://desopve02.proxmox.community:8006 la stessa chiave viene rifiutata:
Login failed: authentication failure (401)L’origin desopve02.… non è un sottodominio dell’origin configurata (desopve01.…). Seconda prova: origin al livello del dominio, con allow-subdomains=1 (opzione che nella GUI non c’è: si imposta da CLI o in /etc/pve/datacenter.cfg):
pvesh set /cluster/options --webauthn "rp=Proxmox Community Lab,origin=https://proxmox.community:8006,id=proxmox.community,allow-subdomains=1"grep webauthn /etc/pve/datacenter.cfgwebauthn: rp=Proxmox Community Lab,origin=https://proxmox.community:8006,id=proxmox.community,allow-subdomains=1Ora la stessa chiave funziona su tutti i nodi: il login con la chiave riesce sia da https://desopve01.proxmox.community:8006 sia da https://desopve02.proxmox.community:8006.
3. Registrare la chiave di sicurezza
Sezione intitolata “3. Registrare la chiave di sicurezza”L’utente stesso, dal menu utente → TFA → Add → WebAuthn, dà una descrizione, conferma la propria password (richiesta perché ha già un altro fattore) e preme Register Webauthn Device: il browser chiede di toccare la chiave.

4. I codici di recupero
Sezione intitolata “4. I codici di recupero”Sempre da TFA → Add → Recovery Keys si generano 10 codici monouso. Vanno stampati o salvati in un luogo sicuro subito: non verranno più mostrati.

L’utente ha ora tre fattori:

5. Il login con più fattori
Sezione intitolata “5. Il login con più fattori”Dopo la password, la finestra propone tutti i fattori dell’utente; si sceglie quello che si ha in mano:

Con WebAuthn la GUI attende la chiave:

Con Recovery Key la GUI mostra quali codici sono ancora disponibili (qui tutti, ID 0-9):

Dopo il login, il codice usato non vale più.
6. Blocco dopo troppi codici sbagliati
Sezione intitolata “6. Blocco dopo troppi codici sbagliati”La 2FA si appoggia anche all’API. Il primo passo del login restituisce un ticket “a metà”, che chiede il secondo fattore:
N=https://172.27.199.21:8006/api2/jsoncurl -sk --data-urlencode "username=stagista@pve" --data-urlencode "password=<password-lab>" $N/access/ticket{"data":{"NeedTFA":1,"username":"stagista@pve","ticket":"PVE:!tfa!{\"totp\"%3Atrue}:6AF2F3BC::…","CSRFPreventionToken":"…"}}Il secondo passo manda il ticket come tfa-challenge e il codice come password=totp:<codice>. Simuliamo un attacco: 10 codici sbagliati, poi quello giusto.
T=<ticket del passo 1>for i in $(seq 10); do printf 'tentativo %s: ' $i curl -sk -w ' HTTP %{http_code}\n' --data-urlencode "username=stagista@pve" --data-urlencode "tfa-challenge=$T" \ --data-urlencode "password=totp:000000" $N/access/ticket; donecurl -sk -o /dev/null -w 'HTTP %{http_code}\n' --data-urlencode "username=stagista@pve" --data-urlencode "tfa-challenge=$T" \ --data-urlencode "password=totp:$(oathtool --totp -b <segreto-base32>)" $N/access/tickettentativo 1: {"message":"authentication failure\n","data":null} HTTP 401…tentativo 10: {"message":"authentication failure\n","data":null} HTTP 401HTTP 401Anche il codice giusto viene rifiutato: dopo 8 tentativi falliti il TOTP dell’utente è bloccato.
pvesh get /access/users --full 1 --output-format json | python3 -c 'import sys,json; [print(u["userid"], {k:u[k] for k in u if "lock" in k}) for u in json.load(sys.stdin) if u["userid"]=="stagista@pve"]'stagista@pve {'totp-locked': 1}Nella GUI la colonna TFA mostra TOTP Locked e il pulsante Unlock TFA sblocca l’utente; da CLI:

pveum user tfa unlock stagista@pveDopo lo sblocco (e un nuovo codice) il login riesce: HTTP 200. Il TOTP si sblocca anche entrando con un codice di recupero. Il blocco protegge dagli attacchi a forza bruta sulle 6 cifre del codice.
7. Token, API e root@pam
Sezione intitolata “7. Token, API e root@pam”I token non passano dalla 2FA. Creiamo un token per stagista@pve (senza separazione dei privilegi, quindi con i permessi dell’utente) e leggiamo il pool web:
V=$(pveum user token add stagista@pve lettura --privsep 0 --output-format json | python3 -c 'import sys,json;print(json.load(sys.stdin)["value"])')curl -sk -w ' HTTP %{http_code}\n' -H 'Authorization: PVEAPIToken=stagista@pve!lettura='"$V" $N/pools/web \ | python3 -c 'import sys,json; t=sys.stdin.read(); j,c=t.rsplit(" HTTP ",1); print(sorted(m["vmid"] for m in json.loads(j)["data"]["members"] if "vmid" in m), "HTTP"+c.strip())'[104, 105, 106] HTTP200Il token legge le risorse senza alcun secondo fattore. Alla fine lo rimuoviamo: pveum user token remove stagista@pve lettura.
Gli script quindi usano token con permessi minimi (Lab 44), non la password di un utente: con la 2FA attiva il login con password via API non basta più (NeedTFA: 1).
TOTP su root@pam. Lo attiviamo per prova:
pvesh create /access/tfa/root@pam --type totp --description 'Prova TOTP su root' \ --totp 'otpauth://totp/root?secret=<segreto>' --value <codice>{"id":"a3071529-f096-4fdb-9680-da3a11afa22e"}Il login API con la sola password ora si ferma a metà, con "NeedTFA":1 come per stagista@pve; SSH e la CLI locale del nodo invece funzionano come prima:
ssh root su desopve02: ok VMID NAME STATUS MEM(MB) BOOTDISK(GB) PID 105 web02 running 2048 8.00 289894pvesh locale: 9.2.26Alla fine togliamo il TOTP di prova da root:
for id in $(pvesh get /access/tfa/root@pam --output-format json | python3 -c 'import sys,json; [print(e["id"]) for e in json.load(sys.stdin)]'); do pvesh delete /access/tfa/root@pam/$id; done8. 2FA obbligatoria per un realm
Sezione intitolata “8. 2FA obbligatoria per un realm”Un realm può imporre un secondo fattore a tutti i suoi utenti (Datacenter → Permissions → Realms → Edit → Require TFA). Proviamo con OATH/TOTP sul realm AD:
pveum realm modify proxmox.community --tfa type=oathpveum realm list --noborder 1 | grep proxmoxproxmox.community ad Active Directory del laboratorio (LDAPS) oathUn utente del dominio senza chiave configurata non entra più, nemmeno con la password giusta. Nel log del nodo:
authentication failure; rhost=<IP del client> [email protected] msg=missing required 2nd keysCon la TFA del realm la chiave dell’utente si imposta nel campo Key IDs dell’utente (pveum user modify … --keys). Attenzione al formato:
400 Parameter verification failed.keys: value does not match the regex patternUn segreto base32 da 32 caratteri senza prefisso viene accettato da pveum, ma al login il nodo lo scarta (bad otp response nel log). Funziona un segreto base32 di 16 caratteri (oppure uno esadecimale da 40):
MS=$(head -c 10 /dev/urandom | base32 | tr -d '=') # 16 caratteriOra il login di Mario chiede il codice TOTP, e con il codice giusto entra:

Togliamo i fattori di prova di stagista@pve (TOTP, WebAuthn e codici di recupero), dalla GUI (Datacenter → Permissions → Two Factor → Remove) o dalla CLI:
for id in $(pvesh get /access/tfa/stagista@pve --output-format json | python3 -c 'import sys,json; [print(e["id"]) for e in json.load(sys.stdin)]'); do pvesh delete /access/tfa/stagista@pve/$iddonepvesh get /access/tfa/stagista@pve --output-format json[]Riportiamo il realm come prima. La chiave di Mario resta però elencata tra i fattori come <old userconfig key 0> anche dopo averla svuotata: va tolta anche da lì.
pveum realm modify proxmox.community --delete tfapvesh get /access/tfa --output-format jsonpvesh get /access/tfa --output-format json[{"entries":[{"created":1794281054,"description":"<old userconfig key 0>","id":"dee874ef-ef5f-45fa-9468-165f95517c49","type":"totp"}],"userid":"[email protected]"}][]Verifica e test
Sezione intitolata “Verifica e test”| Prova | Risultato atteso | Esito |
|---|---|---|
Login di stagista@pve con password + TOTP |
accesso | ok |
| Chiave WebAuthn con origin di un solo nodo, login da un altro nodo | 401 |
ok |
Origin di dominio + allow-subdomains=1 |
la stessa chiave funziona su desopve01 e desopve02 |
ok |
| Login con un codice di recupero | accesso; il codice non vale più | ok |
| 10 codici TOTP sbagliati, poi quello giusto | rifiutato, totp-locked: 1 |
ok |
pveum user tfa unlock |
login di nuovo possibile | ok |
| API token di un utente con 2FA | HTTP 200 senza secondo fattore |
ok |
TOTP su root@pam |
login API NeedTFA, SSH e CLI locale funzionanti |
ok |
| Realm con TFA OATH, utente senza chiave | missing required 2nd keys |
ok |
Stato finale del lab
Sezione intitolata “Stato finale del lab”- WebAuthn del datacenter:
rp=Proxmox Community Lab,origin=https://proxmox.community:8006,id=proxmox.community,allow-subdomains=1. - Fattori registrati: nessuno. I fattori di prova di
stagista@pve(TOTP, WebAuthn, codici di recupero), il TOTP di prova diroot@pame la chiave dimario.rossisono stati rimossi. - Realm
proxmox.community: senza TFA imposta, come nel Lab 43. - Permessi e utenti del Lab 44 invariati.
Cosa hai imparato
Sezione intitolata “Cosa hai imparato”- TOTP, WebAuthn e codici di recupero si possono usare insieme; al login si sceglie il fattore che si ha in mano.
- WebAuthn richiede un certificato attendibile e il nome DNS; con origin di dominio e
allow-subdomainsuna sola chiave vale per tutti i nodi. - I codici di recupero si vedono una sola volta e ciascuno vale una volta.
- Troppi codici TOTP sbagliati bloccano il TOTP dell’utente; si sblocca con Unlock TFA o
pveum user tfa unlock. - La 2FA protegge il login con password, non gli API token né SSH e console: gli script usano token con permessi minimi.
- La 2FA su
root@pamè utile, ma va accompagnata da codici di recupero e da un accesso di emergenza. - La TFA imposta dal realm usa il campo Key IDs dell’utente, con un formato di chiave preciso; quando la si toglie, la chiave resta tra i fattori e va rimossa a parte.