SSD Nodes Learn 🎉 VPS da $5.50/mese
Guide Matt ConnorDi Matt Connor

Attacchi alla supply chain npm sul tuo server

Scopri come un pacchetto npm dannoso raggiunge una Node app su VPS: release patch malevole, script postinstall, typosquat e il deploy che li blocca.

Che cos’è un attacco alla supply chain di npm sul server

Un attacco alla supply chain di npm raggiunge il server tramite un pacchetto che hai scelto di installare. Non sono coinvolte porte aperte né una fase di exploit. npm (node package manager) installa codice e l’installazione del codice ne comporta l’esecuzione. Di conseguenza, una piccola applicazione Node può trascinarsi dietro diverse centinaia di pacchetti che non hai mai esaminato, e uno qualsiasi di questi può pubblicare una nuova versione tra un’ora.

Il deploy scarica una versione dannosa perché il comando di installazione richiede la versione più recente compatibile. Quel codice viene quindi eseguito con i privilegi dell’utente che ha eseguito l’installazione. Tutto ciò che segue deriva da queste due frasi.

I casi sono ordinati in base alla frequenza con cui colpiscono chi esegue il deploy di una singola applicazione Node su un singolo VPS. Non è l’ordine che adotterebbe una grande azienda, perché una grande azienda dispone di un registry interno, di un team di revisione e di un mirror del registry pubblico. Tu hai uno script di deploy.

Forma 1: un account di manutenzione viene compromesso e pubblica una patch

Il registro npm non consente di modificare il contenuto di una versione già esistente. Un attaccante che sottrae le credenziali a un maintainer o ruba un token di pubblicazione non può quindi riscrivere 4.18.2. Pubblica 4.18.3.

Controlla il tuo package.json. Una riga come "express": "^4.18.2" non significa la versione 4.18.2. Il carattere caret significa «qualsiasi versione 4.x uguale o superiore a questa», mentre ~4.18.2 significa «qualsiasi versione 4.18.x». npm install risolve quell’intervallo nel momento dell’esecuzione. Di conseguenza, lo stesso commit Git, distribuito due volte nello stesso pomeriggio, può installare due insiemi di codice diversi. Questo intervallo è la superficie di attacco. Perché si apra non è necessario che qualcosa sul tuo computer venga compromesso.

Le release dannose vengono solitamente segnalate e ritirate, ma il ritiro avviene dopo che sono state installate. Chi ha eseguito il deploy durante l’intervallo conserva il codice sul disco. Una pipeline che risolve gli intervalli a ogni esecuzione entra automaticamente in quell’intervallo diverse volte alla settimana, senza che nessuno lo decida.

Forma 2: uno script di installazione viene eseguito dall'utente che esegue il deploy

Il package.json di un pacchetto può dichiarare preinstall, install, postinstall e prepare nel relativo blocco scripts. npm li esegue durante l'installazione. Non sono isolati in una sandbox e nessuno li esamina. Sono comandi shell eseguiti dall'utente che ha digitato il comando di installazione, nella home directory di quell'utente, con l'accesso di rete dell'utente e l'intero ambiente della shell.

La domanda utile, quindi, non è che cosa può fare il pacchetto. È quali dati può leggere quell'utente. In un normale server usato per il deploy, la risposta include ~/.npmrc, che contiene un token del registry, ~/.ssh/id_ed25519, usato come chiave di deploy per SSH (secure shell), ~/.aws/credentials, ~/.docker/config.json e ogni variabile esportata nella shell, che è il luogo in cui di solito si trova DATABASE_URL.

Un payload di questo tipo non richiede persistenza né un'escalation dei privilegi. Legge alcuni file, li invia a un host tramite HTTPS e termina con lo stato 0. Non si vede nulla perché npm nasconde l'output degli script di installazione per impostazione predefinita. Disabilitare questa opzione consente di vedere che cosa viene eseguito:

npm ci --foreground-scripts

foreground-scripts condivide input, output ed errori standard con il processo npm. Di conseguenza, gli script di compilazione stampano il proprio output nel terminale invece di scriverlo in un buffer che npm scarta quando l'installazione termina correttamente.

Forma 3: typosquat e il nome digitato in modo errato

Un typosquat è un pacchetto pubblicato con un nome simile a quello di un pacchetto noto, in attesa che qualcuno esegua un comando di installazione digitato o incollato in modo errato. Il meccanismo riguarda il comando, non il codice. Per questo un lockfile non è sufficiente: basta aggiungere una volta il nome errato e, da quel momento, il lockfile lo blocca correttamente.

La variante che colpisce i team, non solo i singoli utenti, è il dependency confusion. Il pacchetto interno si chiama billing-utils e risiede in un registry privato. Se nel registry pubblico non esiste nulla con il nome billing-utils, chiunque può pubblicare un pacchetto con quel nome. npm risolve i nomi senza scope usando il registry pubblico predefinito, quindi può prevalere la copia pubblica. La soluzione consiste nell'usare uno scope di cui siete proprietari e una mappatura del registry per quello scope, in .npmrc:

@yourorg:registry=https://npm.yourorg.example/
//npm.yourorg.example/:_authToken=${NPM_TOKEN}

A questo punto @yourorg/billing-utils viene recuperato esclusivamente da quell'host, perché la mappatura scope-registry viene consultata prima del registry predefinito. Un nome interno senza scope non ha alcuna mappatura e quindi non dispone di questa protezione.

Prima di aggiungere una nuova dipendenza, esaminatela invece di limitarvi a guardare il relativo badge dei download:

npm view some-lib repository.url maintainers time.created time.modified

Un pacchetto creato il mese scorso e pubblicato da un account che non potete collegare a un repository pubblico presenta un rischio diverso rispetto a un pacchetto con una storia di sei anni. Nessuno dei due elementi costituisce una prova. Entrambi sono semplici da verificare.

Forma 4: la dipendenza il cui proprietario è cambiato senza preavviso

I manutentori trasferiscono la gestione dei pacchetti. Qualcuno abbandona il progetto, un estraneo offre il proprio aiuto, i diritti di pubblicazione passano a un'altra persona e nessuna notifica raggiunge i progetti che dipendono dal pacchetto. Non si è verificata alcuna compromissione. La fiducia che avevi accordato nel 2021 ora è nelle mani di un'altra persona.

Questa è la forma più lenta e più difficile da rilevare. Nessun comando fornisce una risposta diretta. Due controlli consentono di restringere il campo. Prima di adottare un pacchetto, verifica chi può pubblicarlo usando la riga npm view riportata sopra. Poi esamina il diff quando cambia un pacchetto da cui dipendi effettivamente:

npm diff --diff-name-only --diff=some-lib@1.4.2 --diff=some-lib@1.4.3
npm diff --diff=some-lib@1.4.2 --diff=some-lib@1.4.3

La prima forma mostra solo i nomi dei file modificati. È quindi abbastanza rapida da eseguire a ogni aggiornamento di un pacchetto importante. Prima che raggiunga il server, vale la pena leggere per intero una patch release che modifica uno script di build, aggiunge un file nella directory radice del pacchetto o modifica il blocco scripts.

Creare a partire da un lockfile sottoposto a commit con npm ci

package-lock.json registra la versione esatta di ogni pacchetto nell'albero, l'URL di provenienza, un hash di integrità sha512 per ogni tarball e il pacchetto che lo ha richiesto. Sottoponetelo a commit. È l'unico file che indica ciò che avete effettivamente testato.

Quindi eseguite l'installazione con npm ci, mai con npm install, su qualsiasi macchina che non sia un laptop di sviluppo:

npm ci --omit=dev --ignore-scripts

npm ci differisce da npm install in aspetti che qui sono tutti rilevanti. Richiede che esista un lockfile. Prima di iniziare rimuove qualsiasi node_modules esistente, quindi i residui di un deploy precedente non possono rimanere in quello corrente. Non scrive mai in package.json né nel lockfile, quindi un'installazione non può avanzare silenziosamente a una versione successiva. Se il lockfile e package.json non corrispondono, termina con un errore invece di risolvere la differenza.

Questo errore è una funzionalità, non un inconveniente. Significa che una modifica alle dipendenze deve arrivare tramite un commit revisionato, non come effetto collaterale di un deploy alle 02:00.

L'hash di integrità viene verificato a ogni download. Se i byte di un tarball non corrispondono all'hash registrato, l'installazione termina con code EINTEGRITY invece di decomprimerlo. È importante precisare cosa garantisce: dimostra che il file ricevuto è quello fissato dal lockfile. È la stessa garanzia fornita da verificare i download con checksum e presenta gli stessi limiti. Non indica se la versione fissata fosse dannosa al momento della pubblicazione.

Un dettaglio su --omit=dev: questi pacchetti vengono comunque risolti e scritti nel lockfile. Semplicemente, non vengono collocati su disco. Meno pacchetti su disco significa meno script di installazione e meno codice caricato a runtime, quindi conviene farlo. Questo non rimuove una dipendenza dall'albero.

Tratta gli script di installazione come codice e impara a rifiutarli

Puoi disattivare gli script di installazione. Inserisci questa impostazione nel .npmrc del progetto e sottoponila a commit insieme al lockfile:

ignore-scripts=true
save-exact=true

ignore-scripts=true impedisce a npm di eseguire gli script dichiarati nelle dipendenze. save-exact=true fa sì che npm install some-lib scriva 1.4.2 in package.json invece di ^1.4.2, così un intervallo di versioni non entra accidentalmente nel manifest.

Questa impostazione può causare malfunzionamenti. Devi sapere come gestirli prima di abilitarla. I pacchetti che compilano un addon nativo o scaricano un binario precompilato eseguono queste operazioni in uno script di installazione. Con gli script disattivati, l'installazione termina correttamente, ma l'errore compare in seguito, durante l'esecuzione, sotto forma di un modulo che non riesce a caricare il proprio file di binding. La soluzione è usare un'allowlist:

npm ci --ignore-scripts
npm rebuild better-sqlite3

npm rebuild <package> esegue gli script di compilazione per quel singolo pacchetto. In questo modo prendi una decisione per ogni pacchetto, invece di concedere autorizzazioni di esecuzione indiscriminate a qualche centinaio di soggetti sconosciuti.

Per verificare quanto sia ampia attualmente questa autorizzazione, interroga npm:

npm query ":attr(scripts, [postinstall])"

Il comando stampa ogni pacchetto dell'albero installato che contiene uno script postinstall. In un'applicazione tipica, l'elenco è più breve di quanto ci si aspetti. È proprio questo che rende pratica l'allowlist.

Separare la build dal processo che gestisce il traffico

L'utente di deploy deve poter scrivere in node_modules. Il processo che risponde alle richieste HTTP non deve poterlo fare. Se usano lo stesso account, il codice eseguito durante l'installazione può riscrivere il codice che serve gli utenti, e anche il codice eseguito a runtime può riscriverlo.

Separare i due ruoli. Eseguire la build con un utente, servire l'applicazione con un altro e rendere la directory pubblicata di sola lettura per l'account che gestisce il servizio:

sudo useradd --system --home-dir /srv/nodeapp --shell /usr/sbin/nologin nodeapp
sudo chown -R deploy:nodeapp /srv/nodeapp
sudo chmod -R o-rwx /srv/nodeapp

Lasciare quindi che sia systemd ad applicare queste restrizioni. Scrivere /etc/systemd/system/nodeapp.service:

[Unit]
Description=Node application
After=network-online.target

[Service]
User=nodeapp
Group=nodeapp
WorkingDirectory=/srv/nodeapp/current
EnvironmentFile=/etc/nodeapp/env
ExecStart=/usr/bin/node server.js
Restart=on-failure

NoNewPrivileges=yes
ProtectSystem=strict
ProtectHome=yes
PrivateTmp=yes
ReadWritePaths=/srv/nodeapp/shared
NoExecPaths=/srv/nodeapp/shared
RestrictAddressFamilies=AF_INET AF_INET6 AF_UNIX

[Install]
WantedBy=multi-user.target

ProtectSystem=strict monta l'intero file system in sola lettura per questo servizio, ad eccezione di /dev, /proc, /sys e di tutti i percorsi elencati in ReadWritePaths. Di conseguenza, un tentativo dell'applicazione di scrivere in node_modules fallisce con EROFS: read-only file system. È possibile verificarlo nei propri log in circa un minuto. NoExecPaths gestisce la directory scrivibile per gli upload: il servizio può scrivervi file, ma il kernel ne impedisce l'esecuzione. Questa opzione richiede systemd 249 o versioni successive; Ubuntu 24.04 include la versione 255.

In questo file dell'unità ci sono due aspetti critici. Primo: non aggiungere MemoryDenyWriteExecute=yes. Questa direttiva compare nella maggior parte degli elenchi di hardening di systemd e impedisce l'avvio di Node, perché V8 compila JavaScript in codice macchina a runtime e richiede pagine che siano contemporaneamente scrivibili ed eseguibili. Secondo: ricavare il percorso ExecStart da command -v node. Se Node è stato installato con un gestore di versioni, si trova nella directory home dell'utente di deploy. ProtectHome=yes nasconde quindi quella directory al servizio e l'unità fallisce immediatamente con status=203/EXEC, mentre nei log compare una riga che indica che non è stato possibile trovare l'eseguibile.

Verificare il risultato invece di affidarsi al file:

sudo systemctl daemon-reload
sudo systemctl enable --now nodeapp
sudo systemd-analyze security nodeapp.service
sudo -u nodeapp touch /srv/nodeapp/current/probe

systemd-analyze security elenca tutte le impostazioni di hardening e il relativo livello di esposizione, così è possibile vedere quali sono ancora al valore predefinito. touch dovrebbe fallire con Permission denied, perché nodeapp non possiede alcun elemento sotto current. Se l'operazione riesce, la proprietà dei file è errata e le impostazioni di systemd stanno nascondendo il problema.

Una precisazione su EnvironmentFile: systemd lo legge con privilegi di root prima di passare a User=nodeapp, quindi il file può avere i permessi root:root con modalità 600. L'applicazione riceve comunque le variabili. Chiunque disponga di una shell come nodeapp può ancora leggerle da /proc/<pid>/environ; questa protezione quindi tutela il secret quando è memorizzato, non il processo in esecuzione.

Mantieni le credenziali di deploy fuori dall'ambiente di build

Gli script di installazione ereditano l'ambiente. Questo fatto determina dove eseguire la build.

La soluzione più sicura consiste nell'eseguire la build su una macchina diversa dal server di produzione e copiare quindi la directory risultante. La macchina di build contiene soltanto un token di registro in sola lettura. Non contiene una chiave SSH per il deploy, una chiave di accesso cloud, una password del database né le credenziali di accesso al registro dei container.

npm token create --read-only

Un token in sola lettura può scaricare i pacchetti, ma non può pubblicarli. Se viene sottratto dall'ambiente di build, la perdita consiste nella possibilità di scaricare pacchetti pubblici.

Se devi eseguire la build sul server, eseguila come utente deploy con un ambiente volutamente limitato e conserva i secret di runtime in /etc/nodeapp/env, che deploy non può leggere. Lo stesso principio si applica all'automazione di build che gestisci direttamente: un runner GitHub Actions self-hosted contiene token ed esegue codice pubblicato arbitrario a ogni job, diventando così la macchina di maggior valore in un ambiente di deploy ridotto. Qualsiasi programma che non hai scritto tu e che riceve l'intero ambiente deve essere trattato allo stesso modo. Per questo mantenere i secret fuori dall'ambiente di un agente AI equivale allo stesso problema con un programma diverso al centro.

Bloccare una dipendenza o includere il pacchetto nel repository

Una dipendenza bloccata è una dipendenza la cui versione non può cambiare senza un commit. Il lockfile versionato applica già questo vincolo all'intero albero. In due casi sono necessarie misure aggiuntive.

Il primo caso riguarda le dipendenze transitive. Non controlli da quali pacchetti dipendono le tue dipendenze. overrides in package.json forza una versione in qualsiasi punto dell'albero:

{
  "overrides": {
    "some-transitive-lib": "1.4.2"
  }
}

Esegui npm install una volta dopo aver aggiunto questa configurazione, in modo che il lockfile registri il risultato, quindi esegui il commit di entrambi i file.

Il secondo caso riguarda un pacchetto che non puoi verificare e che non puoi rimuovere. Includilo nel repository. npm pack scarica l'archivio tar esatto che il registry distribuirebbe, mentre una dipendenza file: viene installata dalla copia locale:

npm pack some-lib@1.4.2
mkdir -p vendor && mv some-lib-1.4.2.tgz vendor/
{
  "dependencies": {
    "some-lib": "file:vendor/some-lib-1.4.2.tgz"
  }
}

L'archivio tar si trova ora nel repository e non può cambiare senza il tuo controllo. Tuttavia, da questo momento devi occuparti per sempre dei suoi aggiornamenti. Usa questa soluzione per il piccolo pacchetto abbandonato da cui dipendi, non per il tuo framework web.

Esiste anche un periodo di attesa, che non costa nulla:

npm install --before=2026-08-01

L'opzione before ricostruisce l'albero usando soltanto le versioni pubblicate entro quella data. Quando aggiorni le dipendenze, imposta una data di una o due settimane precedente. In questo modo eviti la finestra in cui una release compromessa è disponibile ma non è ancora stata segnalata. È uno strumento poco selettivo, perché ritarda anche gli aggiornamenti di sicurezza corretti. Usalo per risolvere gli intervalli di versione, verifica le modifiche, quindi esegui il commit del lockfile.

Come posso sapere quale versione ho effettivamente distribuito?

Il lockfile in git indica cosa avrebbe dovuto essere installato. Il disco indica cosa è installato. Solo il secondo dato costituisce una prova.

npm ls some-lib
node -e "console.log(require('./node_modules/some-lib/package.json').version)"

npm ls legge node_modules, quindi segnala ciò che è fisicamente presente invece di ciò che il lockfile prevedeva. La riga node -e legge il manifest installato tramite il percorso, quindi funziona anche per i pacchetti il cui campo exports impedisce gli import di sottopercorsi e stampa una sola versione, senza il disegno ad albero intorno.

Per l'altra parte del confronto, leggi git:

git log --oneline -- package-lock.json
git show <commit>:package-lock.json | grep -A3 '"node_modules/some-lib"'

Rendi permanente il collegamento tra i due dati inserendo il commit nel layout di deploy. Rilascia in /srv/nodeapp/releases/<short commit sha> e punta /srv/nodeapp/current a quella directory tramite un symlink. La risposta alla domanda "cosa è attualmente in esecuzione" diventa readlink /srv/nodeapp/current ed è disponibile alle 03:00 anche per chi non ha eseguito il deploy.

Infine, verifica cosa il registry è in grado di attestare:

npm audit signatures

Questa operazione verifica le firme del registry sui pacchetti presenti nell'albero installato e verifica le attestazioni di provenienza per i pacchetti che le includono. La provenienza collega un tarball pubblicato alla build pubblica di continuous integration (CI) che lo ha prodotto. Un'attestazione verificata consente quindi di risalire dal codice a un commit, anziché a un laptop sconosciuto. La copertura non è universale, quindi interpreta un'attestazione mancante come "nessuna informazione", non come "pacchetto non affidabile".

Cosa fare dopo che una release compromessa raggiunge il server

Partite da ciò che è stato eseguito e dall'utente con cui è stato eseguito.

Se il codice è stato eseguito durante l'installazione, presumete che tutto ciò che era leggibile dall'utente di build sia compromesso. Ruotate il token del registry, le chiavi SSH presenti nella relativa home directory, le credenziali cloud e ogni secret esportato in quella shell. La rotazione è l'unica risposta corretta, perché non potete dimostrare che un file non sia stato letto.

Se il codice è stato eseguito a runtime con un account di servizio con privilegi limitati, l'insieme delle risorse raggiungibili è molto più piccolo: le variabili d'ambiente dell'applicazione e tutto ciò che è accessibile tramite la sua connettività di rete. Questo è il motivo per cui conviene eseguire i servizi come utenti senza privilegi su un VPS. Non impedisce la compromissione. Determina quale parte della macchina può raggiungere la compromissione e se questa persiste dopo un riavvio.

Ricostruite quindi l'ambiente invece di ripulirlo. Eliminate node_modules, bloccate il pacchetto interessato a una versione precedente a quella compromessa in package.json, eseguite npm install una sola volta per aggiornare il lockfile, eseguite il commit e fate il deploy con npm ci. Non riparate un tree in place. Non potete enumerare tutto ciò che uno script di installazione ha modificato.

Annotate anche l'intervallo temporale: il primo deploy che avrebbe potuto prelevare la versione e il deploy che l'ha rimossa. Questo intervallo indica quali log interni consultare ed è determinabile solo se le release hanno un nome basato sui commit.

Cosa non risolve nessuna di queste misure

Un lockfile non rende sicura una dipendenza. Trasforma il momento in cui hai accettato quella dipendenza in una decisione datata e verificata, invece che in un effetto collaterale di un deploy. Ogni pratica descritta sopra esegue la stessa trasformazione: da incidente a scelta consapevole.

npm audit non è una difesa in questo caso. Confronta il tuo albero delle dipendenze con un database di vulnerabilità segnalate, quindi rileva i problemi che sono già stati pubblicati e identificati. Un attacco alla supply chain resta senza nome per tutta la sua vita utile. Esegui npm audit per individuare i vecchi bug noti, ma non aspettarti che rilevi una release pubblicata quattro ore fa.

Ridurre il numero di dipendenze aiuta più di qualsiasi strumento descritto in questa guida, ma è il consiglio meno popolare. Ogni pacchetto che non aggiungi è un publisher in meno che può essere vittima di phishing per tuo conto e uno script di installazione in meno che viene eseguito con il tuo deploy user.

Nessuna di queste considerazioni riguarda solo npm. Le stesse quattro situazioni si applicano a PyPI, RubyGems, alle immagini dei container e al package manager della tua distribuzione. Il problema emerge soprattutto con npm perché gli alberi delle dipendenze sono più profondi e gli script di installazione vengono eseguiti per impostazione predefinita. La parte della macchina circostante che devi proteggere dipende dall'ambiente in cui il servizio viene eseguito. Questo rientra nella questione più ampia della sicurezza dell'hosting VPS.

FAQ

npm ci protegge da un pacchetto npm compromesso?

Protegge dal cambio di versione a tua insaputa. npm ci installa esattamente ciò che registra package-lock.json, verifica ogni tarball rispetto al relativo hash di integrità sha512 e termina con un errore se package.json e il lockfile non corrispondono, invece di risolvere la differenza. Non fornisce alcuna garanzia sulla sicurezza della versione bloccata. Se esegui il commit di un lockfile che blocca una versione dannosa, npm ci installerà correttamente quella versione su ogni server che gestisci, ogni volta.

Devo impostare ignore-scripts=true per tutto?

Impostalo, quindi aggiungi le eccezioni a una allowlist. ignore-scripts=true nel file .npmrc del progetto impedisce l'esecuzione degli script di installazione delle dipendenze. In questo modo rimuove il percorso più diretto da un pacchetto dannoso alle credenziali dell'utente di deploy. I pacchetti che compilano un addon nativo o scaricano un binario precompilato hanno realmente bisogno dei propri script. Con gli script disabilitati, però, falliscono in seguito durante l'esecuzione perché manca il file di binding, invece di fallire durante l'installazione. Esegui npm ci --ignore-scripts, quindi npm rebuild <package> per i pochi pacchetti di cui hai deciso di fidarti. npm query ":attr(scripts, [postinstall])" mostra quanti sono realmente.

Come posso sapere quale versione di un pacchetto è stata effettivamente installata sul server?

Leggi ciò che è presente sul disco, non il lockfile. npm ls <package> indica ciò che è presente in node_modules, mentre node -e "console.log(require('./node_modules/<package>/package.json').version)" stampa soltanto la stringa della versione. Il lockfile in git risponde a una domanda diversa: quale versione avrebbe dovuto essere installata. Il confronto tra i due valori è quindi essenziale. Eseguire il deploy in una directory denominata con il commit git mantiene disponibili entrambe le informazioni anche mesi dopo, quando servono.

npm audit rileva gli attacchi alla supply chain?

No. npm audit confronta il tuo albero delle dipendenze con un database di vulnerabilità segnalate. Rileva quindi soltanto i problemi già pubblicati e associati a un identificatore. Una release dannosa può non essere segnalata durante le ore o i giorni in cui la sua installazione è rilevante. npm audit signatures è il comando più utile: verifica le firme del registry nell'intero albero installato e controlla le attestazioni di provenienza quando il publisher le ha prodotte. In questo modo puoi verificare che un tarball provenga da una build pubblica e non da una macchina sconosciuta.

Perché è importante eseguire l'applicazione con un utente senza privilegi se l'attacco avviene durante l'installazione?

Perché i due problemi hanno portata diversa e devi difenderti da entrambi. Il codice eseguito durante l'installazione viene eseguito come utente di deploy e può leggere le chiavi SSH, i token del registry e le credenziali cloud di quell'utente. Il codice eseguito durante il runtime viene eseguito come account del servizio. Con User=nodeapp, ProtectSystem=strict e senza credenziali sul disco, la sua portata si limita all'ambiente dell'applicazione e al relativo database. Separare gli account impedisce inoltre al processo che gestisce il traffico di riscrivere node_modules. In questo modo una compromissione durante il runtime viene eliminata al riavvio successivo invece di diventare permanente.