Su un portatile con GPU integrata Intel, un servizio Ollama utente è sceso da un’esposizione di 9.4 a 2.2 senza perdere l’accelerazione. Il risultato vale per questa macchina, ma il metodo è riusabile. Il guasto più insidioso della prova, infatti, non ha prodotto né errori né warning.
Il punto di partenza
ollama.service gira come servizio utente su CachyOS, con systemd 262.1 e Ollama 0.40.9. Ascolta su 127.0.0.1:11434, usa la GPU via Vulkan attraverso /dev/dri e tiene modelli e chiave privata id_ed25519 in ~/.ollama. La unit contiene ExecStart=, Restart=on-failure, After=network-online.target, i limiti MemoryMax=24G e MemorySwapMax=2G e alcune variabili d’ambiente. Non ha direttive di hardening.
systemd-analyze --user security ollama.service assegna 9.4 UNSAFE. Il manuale chiarisce come leggere il dato: un’esposizione alta indica poco sandboxing applicato da systemd, non che il servizio sia vulnerabile.
Il manuale avverte anche che alcune impostazioni di namespace non funzionano da sole nei servizi utente, e che la maggior parte funziona insieme a PrivateUsers=true. Il test quindi non poteva fermarsi al punteggio: andava controllato anche il processo reale.
Il metodo
Le direttive sono state aggiunte a gruppi cumulativi in un drop-in separato, con un riavvio a ogni passo. Se un gruppo falliva, le sue direttive venivano riprovate una per una. Così una rottura si può ricondurre al gruppo responsabile, senza cercarla tra tutte le direttive.
A ogni passo sono stati eseguiti quattro controlli:
- una risposta di chat;
- il modello caricato al 100% in VRAM;
- un embedding;
- una scrittura in
~/.ollama, copiando ed eliminando un modello.
Il secondo controllo è quello decisivo. Un servizio attivo che risponde non dimostra di usare ancora la GPU.
Alla fine il drop-in è stato rimosso e la unit è tornata com’era: la configurazione è stata provata, non adottata in modo permanente.
Cosa ha funzionato
Il punteggio è sceso da 9.4 a 2.2 con la GPU sempre al 100%. I passi che pesano di più sono Capability (-2.0), Kernel e System call (-1.5 ciascuno) e Namespace (-0.8). Filesystem e Home nascosta valgono solo -0.2 e -0.1, anche se sono tra le protezioni più concrete: il peso nel punteggio non coincide con l’utilità pratica.
| Passo cumulativo | Esposizione | GPU | Esito |
|---|---|---|---|
| Nessuna direttiva | 9.4 UNSAFE | 100% | ok |
| Privilegi | 8.9 EXPOSED | 100% | ok |
| Kernel | 7.4 MEDIUM | 100% | ok |
/tmp privata | 7.2 MEDIUM | 100% | ok |
| Filesystem di sola lettura | 7.0 MEDIUM | 100% | ok |
| Home nascosta | 6.9 MEDIUM | 100% | ok |
| Famiglie di indirizzi | 6.5 MEDIUM | 100% | ok |
| Namespace | 5.7 MEDIUM | 100% | ok |
| Capability | 3.7 OK | 100% | ok |
| System call | 2.2 OK | 100% | ok |
MemoryDenyWriteExecute= | 2.2 OK | 100% | ok |
PrivateDevices= (scartata) | 1.9 OK | 0% | fallito |
MemoryDenyWriteExecute=yes era il candidato più sospetto, perché il manuale lo segnala come incompatibile con il codice generato a runtime. Qui non ha causato problemi nei quattro controlli né nelle prove di caricamento e generazione, ma i carichi prolungati non sono stati provati.
Il punteggio legge la configurazione, non il processo
systemd-analyze valuta le impostazioni della unit, quindi 2.2 non dimostra da solo che i limiti siano attivi. Per questo lo stato del processo è stato letto da /proc, con e senza hardening.
| Proprietà del processo | Unit originale | Con hardening |
|---|---|---|
NoNewPrivs | 0 | 1 |
Seccomp | 0 | 2, con 28 filtri |
CapBnd | completo | vuoto |
| Namespace utente e di mount | quelli dell’host | propri |
| Root del filesystem | scrivibile | sola lettura |
~/.ssh e ~/.config | visibili | assenti |
~/.ollama | scrivibile | scrivibile |
Su questa macchina le direttive sono applicate davvero, nonostante l’avvertenza del manuale sui servizi utente: il processo vede solo l’UID 1000, in un namespace utente proprio. Su altri sistemi o kernel il risultato può cambiare, quindi questa lettura va ripetuta. Il namespace di rete invece resta quello dell’host: RestrictAddressFamilies= limita le famiglie di indirizzi, ma non isola il servizio dalla rete.
Cosa si rompe
PrivateDevices=yes è il caso da ricordare. Il manuale spiega che crea un nuovo /dev con i soli pseudo-dispositivi e senza dispositivi fisici. Il punteggio migliora a 1.9, il servizio resta attivo e risponde, ma il modello finisce allo 0% di GPU. La velocità del test scende da 18,4 a 9,9 token al secondo.
Nel journal di quell’avvio non ci sono né warning né errori. L’unico indizio è una riga di livello INFO, inference compute id=cpu library=cpu. Chi controlla solo lo stato del servizio o gli errori non se ne accorge. Il manuale non nomina /dev/dri, quindi il legame con quel percorso è una deduzione coerente con la misura.
Il secondo problema è più sottile. ProtectHome=tmpfs monta un tmpfs di sola lettura sulle home, e Mesa non può più creare la cache degli shader:
Failed to create ~/.cache for shader cache (Read-only file system)---disabling.
La GPU funziona lo stesso, ma nella prova i caricamenti a freddo si sono allungati. Rendendo disponibile solo ~/.cache/mesa_shader_cache con BindPaths= e ReadWritePaths=, l’avviso è sparito e i tempi sono tornati in linea con l’originale.
| Configurazione | Esposizione | Caricamento 35b | Caricamento 4b | Generazione 35b |
|---|---|---|---|---|
| Unit originale | 9.4 | 11,7 s | 4,3 s | 15,4 tok/s |
| Con hardening | 2.2 | 15,6 s | 6,3 s | 14,7 tok/s |
| Con hardening e bind della cache | 2.2 | 10,7 s | 4,0 s | 15,0 tok/s |
I valori sono medie di due run. Le differenze di velocità rientrano nel rumore di misura. Per i caricamenti due run bastano a mostrare la tendenza, non a isolarne la causa: il legame con la cache degli shader è l’ipotesi più coerente.
Cosa il punteggio non dice
Il passo Capability vale 2 punti, ma è in gran parte nominale: il processo aveva già 0 capability effettive, e il bounding set svuotato non cambia ciò che faceva. Il punteggio finale quindi va letto come misura delle protezioni attive, non del rischio residuo.
Restano anche limiti concreti. La chiave id_ed25519 è ancora leggibile dal servizio, perché ~/.ollama deve restare accessibile. Il namespace di rete è quello dell’host: ascoltare su 127.0.0.1 riguarda le connessioni in entrata, quindi non limita di per sé ciò che il processo può contattare in uscita.
Non sono stati provati ollama pull, IPAddressDeny=, UMask=, DeviceAllow= e i carichi prolungati. I dati vengono da un solo portatile e da due run per configurazione. Se i modelli stanno fuori da ~/.ollama, i percorsi vanno adattati.
La configurazione provata
Le direttive sono raggruppate per passo. ReadWritePaths= e BindPaths= compaiono due volte, come nella prova: le righe si sommano. Con ProtectHome=tmpfs il bind è necessario, perché senza di esso quei percorsi non esisterebbero dentro il servizio. ~/.ollama e ~/.cache/mesa_shader_cache devono già esistere prima del riavvio: un percorso mancante può impedire l’avvio della unit.
[Service]
# Privilegi
NoNewPrivileges=yes
RestrictSUIDSGID=yes
LockPersonality=yes
RestrictRealtime=yes
# Kernel
ProtectClock=yes
ProtectKernelTunables=yes
ProtectKernelModules=yes
ProtectKernelLogs=yes
ProtectControlGroups=yes
ProtectHostname=yes
# /tmp privata
PrivateTmp=yes
# Filesystem di sola lettura
ProtectSystem=strict
ReadWritePaths=%h/.ollama
# Home nascosta
ProtectHome=tmpfs
BindPaths=%h/.ollama
# Famiglie di indirizzi
RestrictAddressFamilies=AF_UNIX AF_INET AF_INET6 AF_NETLINK
# Namespace
RestrictNamespaces=yes
# Capability
CapabilityBoundingSet=
AmbientCapabilities=
# System call
SystemCallArchitectures=native
SystemCallFilter=@system-service
SystemCallErrorNumber=EPERM
# Memoria
MemoryDenyWriteExecute=yes
# Cache degli shader
ReadWritePaths=%h/.cache/mesa_shader_cache
BindPaths=%h/.cache/mesa_shader_cache
Il blocco va in un drop-in, per esempio con systemctl --user edit ollama.service, seguito da un riavvio del servizio. Rimuovendo il drop-in e riavviando si torna alla unit originale. Per verificarla bastano tre comandi, dopo aver caricato un modello:
systemd-analyze --user security ollama.service
ollama ps
grep -E 'NoNewPrivs|Seccomp|CapBnd' /proc/$(systemctl --user show ollama -p MainPID --value)/status
La colonna PROCESSOR di ollama ps deve indicare 100% GPU.
Fonti
systemd.exec(5):ProtectSystem=,ProtectHome=,PrivateDevices=,MemoryDenyWriteExecute=,ReadWritePaths=, sezione SANDBOXINGsystemd-analyze(1): comandosecurity