Salta al contenuto

Blog ·

Un confine appartiene al nucleo

Revocare un agente ospitato lo staccava dal suo ordine e lasciava accesa su Proxmox una macchina che nessuno raggiungeva e che tutti continuavano a pagare.

Di Provibr

Un Cloud Agent è la versione del nostro agente in cui la macchina la gestiamo noi. Il cliente ne ordina uno, noi cloniamo un template Proxmox, ci installiamo sopra l’agente e lo fatturiamo ogni mese. Dal pannello sembra un agente qualsiasi, e quella somiglianza è tutta la storia di questo turno.

Ogni altro agente appartiene al cliente. Revocarlo è un’operazione pulita e sicura: la piattaforma manda un avviso di revoca, l’agente cancella la propria identità, il proprio certificato e il proprio caveau di credenziali, e a quel punto la riga si può eliminare. Non resta niente in funzione da nessuna parte, perché la cosa che era in funzione non è mai stata nostra.

Per un Cloud Agent questo non è vero, e nulla lo impediva.

Che cosa abbiamo misurato prima di cambiare qualsiasi cosa

Sulla base dati di sviluppo, con un ordine reale dietro un agente reale, abbiamo chiamato il percorso di rimozione esattamente come fa il pulsante del pannello.

Ha riportato mode: "hard". La riga dell’agente non c’era più. La riga dell’ordine era ancora lì, ancora active, ancora con il vmid 5998, e il suo agent_id era diventato in silenzio NULL, perché la chiave esterna dall’ordine all’agente è ON DELETE SET NULL.

C’era dunque una macchina virtuale su un hypervisor, accesa, con sopra un agente che aveva cancellato la propria identità e non avrebbe quindi mai più potuto connettersi, agganciata a un ordine che continuava a generare un addebito mensile, e nessuna riga da nessuna parte che puntasse dall’una all’altro. Non raggiungibile, non arrestabile dal pannello e non gratuita.

Il vincolo che ha prodotto tutto questo è corretto. Mettere il riferimento a NULL è il comportamento giusto per una colonna che dice quale agente è installato qui adesso; una cascata avrebbe eliminato l’ordine e con esso il record di fatturazione. Il difetto è che nessuno faceva la domanda che lo schema non può fare.

Due protezioni esistenti, entrambe rivolte dalla parte sbagliata

L’eliminazione di un agente aveva già due rifiuti, e vale la pena essere precisi sul perché non ne sia scattato nessuno dei due.

Il primo rifiuta di eliminare un agente attualmente iscritto: lo revochi prima, così si ripulisce da solo. Nel momento in cui la riga veniva eliminata l’agente era appena stato revocato nella stessa chiamata, quindi non era iscritto e il controllo era soddisfatto.

Il secondo rifiuta di eliminare un agente a cui pendono ancora integrazioni o server, perché quelle chiavi esterne cascano e un solo clic si porterebbe via l’intero inventario. Un Cloud Agent non ha né le une né gli altri: è una macchina nuova che esiste per far girare l’agente, quindi il controllo è passato perché non c’era nulla da contare.

Entrambe le protezioni riguardavano ciò che l’eliminazione distruggerebbe. La cosa che sopravvive all’eliminazione, e costa soldi proprio perché sopravvive, stava fuori dalla forma della domanda.

Il confine va nel nucleo, non nel pulsante

La riparazione ovvia è controllare nell’azione server dietro il pulsante. È anche sbagliata, e la ragione è una falla in cui questa base di codice è già finita una volta, con un endpoint di autenticazione preconfezionato che scavalcava ogni controllo lato server.

Abbiamo contato i punti di chiamata. Fuori dalle due funzioni del nucleo ci sono sette chiamate a revoca, eliminazione o rimozione combinata, distribuite su tre file: tre azioni server sulla pagina dell’agente, l’azione di massa e due rotte dell’API v1. Un controllo nell’azione del pannello avrebbe lasciato aperte le rotte dell’API, e una rotta dell’API è esattamente ciò che usa lo script di un cliente.

Il confine sta dunque dentro revokeAgentCore() e deleteAgentCore() stessi, il punto obbligato attraverso cui passano tutte e sette le chiamate. Una funzione, interrogata due volte.

E sta dopo il ramo idempotente, non prima. Un agente già revocato non ha più nulla da revocare, e dire a quel chiamante di annullare prima il suo ordine sarebbe una falsità a proposito di una chiamata che tanto non farà nulla. Un confine appartiene all’operazione che cambia davvero qualcosa.

Il predicato è questo si può ancora annullare, non questo è vivo

Nel pacchetto condiviso c’è un elenco che si chiama CLOUD_AGENT_LIVE_STATUSES: gli stati in cui una macchina esiste e costa soldi. Ha quattro membri ed è esattamente l’elenco sbagliato da riutilizzare qui.

Il predicato che usiamo è cloudAgentActionAllowed("cancel", status), che è vero per sette degli otto stati dell’ordine, tutti tranne un ordine già annullato.

I tre stati che stanno nella differenza sono quelli interessanti, e failed è il più tagliente. Un ordine fallito non costa nulla, quindi non è vivo. Ma un ordine fallito è proprio quello che può essere arrivato abbastanza avanti da creare una macchina e poi essere caduto, ed è per questo che da lì l’annullamento è permesso in primo luogo. Riutilizzare l’elenco della fatturazione avrebbe lasciato senza protezione l’unico caso con una macchina orfana, e in revisione sarebbe sembrato del tutto ragionevole.

Due elenchi che significano quasi la stessa cosa vanno alla deriva. La domanda qui non è questo costa soldi, è l’annullamento è ancora la strada, ed esattamente una funzione nella base di codice risponde a quella.

Due percorsi di scrittura a cui è permesso saltarlo

La protezione che abbiamo scritto ricava ogni punto del sorgente del pannello che revoca, elimina in modo definitivo o nasconde una riga di agente, percorrendo l’albero sintattico dell’applicazione e delle librerie, e pretende per ogni punto o il confine o una ragione scritta. Due punti portano una ragione: il percorso di annullamento stesso, che rimuove la macchina, libera l’indirizzo e soltanto allora revoca l’agente, e la chiusura dell’account, che revoca ogni agente di un’organizzazione e tratta il Cloud Agent a parte nello stesso flusso. Bloccare l’uno o l’altra chiuderebbe un’uscita corretta invece di far risparmiare soldi.

La protezione ha i due setacci che impediscono a uno scanner del sorgente di essere vacuamente verde: un limite inferiore su quanti punti di scrittura deve trovare, tre dei quali nominati, e un test che fallisce su una voce dell’elenco delle eccezioni che punti a un file che non scrive più nulla. Esegue 4 test e 11 asserzioni.

Due fatti sullo schermo, non uno

Il lato del pannello aveva bisogno di due booleani separati e per poco non ne abbiamo pubblicato uno solo.

orderOpen è un fatto sull’ordine: questo si può ancora annullare. canCancel è un fatto su chi guarda: questa persona lo può annullare. Farli collassare in un unico qui si può agire significherebbe che un membro senza il permesso di annullare vede il pulsante di revoca, lo usa, butta via la macchina e non può fermare l’addebito. I pulsanti spariscono quindi in base al fatto sull’ordine, e la scheda resta con dentro la ragione, perché perché questo qui non lo posso revocare è la domanda che si fa chiunque abbia visto il pulsante sui propri altri agenti.

Una cosa è venuta dietro perché la modifica l’ha resa falsa: le istruzioni di disinstallazione consigliavano di revocare prima l’agente nel pannello. È un pulsante che su questa pagina non esiste più.

Che cosa annoteremmo

  1. Una somiglianza nell’interfaccia non è una somiglianza nelle conseguenze. Due agenti, un pulsante, e uno dei due costa soldi dopo che lo si è premuto.
  2. ON DELETE SET NULL può essere giusto e lasciarle comunque un orfano. Lo schema non può fare la domanda.
  3. Le protezioni su ciò che un’eliminazione distrugge non vedranno ciò che un’eliminazione abbandona.
  4. Conti i punti di chiamata prima di scegliere dove va il controllo. I nostri erano sette, in tre file, due dei quali rotte dell’API.
  5. Metta il confine nel punto obbligato, non sul pulsante. Un pulsante è uno degli ingressi.
  6. Lo metta dopo il ramo idempotente. Un rifiuto su una chiamata che non farebbe nulla è una bugia.
  7. Riutilizzare un elenco che significa quasi la stessa cosa è un guasto lento. Costa soldi e si può ancora annullare differiscono per tre stati, e la differenza è dove sta il danno.
  8. Uno stato che non costa nulla può comunque aver lasciato qualcosa dietro di sé. Un provisioning fallito è quello pericoloso.
  9. Separi il fatto sull’oggetto dal fatto su chi guarda. Fusi insieme, consegnano a un membro limitato un pulsante distruttivo.
  10. Lasci che la protezione legga l’albero. Un test per ogni ingresso noto non può vedere l’ingresso che qualcuno aggiungerà il mese prossimo.

Per chiarezza su che cosa sia: il difetto è stato riprodotto contro la base dati di sviluppo prima di qualsiasi modifica, il controllo end to end si è chiuso a 26 verdi ed era stato dimostrato rosso prima a 16 verdi e 10 falliti, il controllo nel browser a 29 verdi e dimostrato rosso a 18 e 11 con il flag dell’ordine forzato a spento, nove lingue a 178 verdi, e la verifica della fusione è girata senza una riga di errore. Non dimostrato: non è stata coinvolta nessuna macchina di un cliente reale, e il lato della fatturazione è stato ispezionato invece che esercitato. Tre trappole di misurazione sono costate tempo e vale la pena nominarle: la zona pericolosa vive dietro un parametro di scheda, quindi un controllo che lo omette misura zero pulsanti e lo legge come un successo; una sequenza di escape con barra rovesciata dentro un template literal passato a una valutazione di pagina non è un escape; e uno dei tre script di catalogo ha appiattito uno spazio unificatore che gli altri due conservavano.

Dal lavoro di piattaforma su Provibr. Perché una macchina si possa vendere esattamente una volta si trova in due ordini, una macchina, perché una scorciatoia sia un riferimento e mai un permesso si trova in una scorciatoia è un riferimento, non un diritto, e una parola di stato presa in prestito dalla tabella sbagliata si trova in una scansione che chiudeva il suo turno.

Inizi con un host e un agente

Crei un account, installi l’agente con un solo comando e colleghi il suo primo host Proxmox. Il periodo di prova dura 14 giorni e non chiede alcun dato di pagamento.