Dal dev locale al deploy in Docker, passando per GitHub Actions. Uno smoke test pratico, con tutti gli inciampi che ho trovato strada facendo.
Un mese fa vi ho raccontato come ho messo fine al caos delle chiavi SSH con 1Password (prima puntata qui). Era la parte facile: una volta capito come funziona l'SSH Agent, il resto scorre da solo.
Oggi torno sull'argomento, ma dall'altro lato, quello che tocca il quotidiano di chiunque lavori in un team: i secrets dei file .env.
"Mi passi il DB_PASSWORD di staging?" "Te lo mando su Slack." "Me lo metti anche nel Drive condiviso"
Come molti dev che lavorano in team piccoli o medi, mi ritrovavo davanti a pattern come questi, sia nei miei progetti personali sia nel quotidiano in ufficio:
file .env.local passati via Slack o Drive, rigenerati a mano a ogni onboarding;
chiavi SSH che restano in ~/.ssh/id_rsa sul laptop molto più a lungo di quanto dovrebbero;
secrets di GitHub Actions inseriti a mano, con la regola non scritta del "l'ultimo che ha deployato sa il valore";
zero audit trail: impossibile sapere chi ha letto cosa e quando.
Presi uno a uno sono inconvenienti piccoli. Sommati, diventano un debito di sicurezza che cresce silenzioso: bus factor a 1 su credenziali importanti, offboarding lenti, tracciabilità zero.
Non è colpa di nessuno in particolare: è entropia. Senza un posto ufficiale dove vivono i secrets, i secrets finiscono nei posti comodi. E prima o poi qualcuno paga il conto.
Da qualche tempo sono entrata nel fantastico team di Boosha, e stiamo iniziando a mettere ordine su questi aspetti, e mi sono ritrovata a esplorare 1Password Business come possibile pezzo di puzzle. L'ho fatto prima sul mio setup personale, per capirne il funzionamento senza fretta; poi ho trasformato quello che stavo imparando in una proposta strutturata per il team, con runbook, esempi e piano di adozione. Ho dedicato una giornata a uno smoke test end-to-end per verificare se il piano regge in uno scenario realistico di sviluppo + deploy + CI. Questo articolo è il racconto di cosa ho scoperto lungo il percorso, e spero possa essere utile a chi, come me, sta pensando di portare una proposta simile nel proprio team.
La visione in tre strati
Prima di entrare nei comandi, serve un modello mentale. Un secret nella vita di un progetto passa da tre contesti, e 1Password li copre tutti con strumenti diversi.
Strato 1. Dev locale. Il dev avvia l'app sul suo laptop. Non vogliamo nessun file .env reale sul disco.
Strato 2. Produzione. L'app parte su un server (systemd, Docker, quello che volete). Vuole un .env standard, non sa nemmeno che 1Password esiste.
Strato 3. CI/CD. GitHub Actions deve deployare, pubblicare un pacchetto npm, mandare una notifica a Slack. Vuole secrets come env var, dentro un job.
Stesso concetto: un secret, un posto dove vive, tre modi di consumarlo. Questa tripartizione è il fulcro di tutto. Se ricordate solo questo, l'articolo avrà funzionato.
I tre strati. 1Password è la source of truth umana; dev locale, produzione e CI consumano i valori con comandi diversi.
Strato 1. Dev locale con op run
Il meccanismo è banale quanto efficace.
Nel repo committo un template con puntatori, mai valori:
# .env.template (committato in repo)
DATABASE_URL=op://Boosha/project-demo/database_url
OPENAI_API_KEY=op://Boosha/project-demo/openai_api_key
JWT_SECRET=op://Boosha/project-demo/jwt_secretCode language:PHP(php)
Sidebar sinistra dell'app desktop (o op vault list da CLI)
project-demo
Titolo dell'item dentro quel vault
Prima riga di ogni entry nel vault
database_url
Nome del field custom dentro l'item
Etichetta della riga "etichetta: valore"
Un paio di cose che ho imparato alla prima volta e che salvano tempo:
Il field name è sempre in inglese, anche se l'app 1Password è in italiano e mostra "Chiave pubblica". Via CLI si scrive public key, non chiave pubblica. Classico mismatch tra UI localizzata e API.
Se vault o item contengono spazi, si possono usare sia le virgolette ("Client Acme") sia la versione con trattini (Client-Acme). Sceglietene una e siate coerenti nel repo.
Il puntatore non contiene mai il valore: solo il percorso. Il valore arriva al momento del render (op inject) o dell'iniezione nel processo (op run), mai prima.
Ok, torniamo al flusso
I valori reali vivono in 1Password, in un item project-demo dentro il vault del progetto:
L'item in 1Password con i field in chiaro. Tutti i valori dello screenshot sono fittizi (demo-password, FAKE-NOT-REAL-KEY, ecc.). È un item creato apposta per lo smoke test.
Per avviare l'app in dev:
op signin # una volta per sessione
op run --env-file=.env.template -- npm run dev # avvia con i valori iniettatiCode language:PHP(php)
op run risolve i puntatori al volo, legge da 1Password, e avvia npm run dev con le env var già popolate. Nessun file .env reale tocca il disco. Se un dev fa echo $DATABASE_URL per curiosità , vede:
Questo è masking automatico: non l'ho configurato io, è il default. op run intercetta ogni tentativo di stampare valori iniettati e li maschera in stdout/stderr. Serve a proteggerti da un console.log distratto in un Sentry report, da un DEBUG attivo in CI, dal collega che screencondivide al meeting.
Quando ho capito che era il default, ho fatto un piccolo "wow" da sola al monitor. È il tipo di dettaglio che ti fa fidare di uno strumento.
Strato 2. Produzione con op inject
Qui cambia il paradigma. In produzione, su un servizio che gira dietro systemd o in un container Docker con ENTRYPOINT già fissato, non posso mettere op run davanti al comando di avvio. L'app si aspetta un .env standard.
Allora genero il file, al momento del deploy, leggendo i valori da 1Password:
Il template è lo stesso pattern del dev, ma dedicato al production vault:
Il template prima del render: 6 puntatori, zero valori.
Dopo op inject, il file sul server:
Lo stesso file, renderizzato. Stessa forma, ma ora con i valori risolti. Dal punto di vista dell'app, è un env file normalissimo.
Il punto chiave, e qui serve resistere alla tentazione di pensare "ma se avessi già AWS Secrets Manager questo non servirebbe": è vero, ma noi non abbiamo AWS Secrets Manager oggi. E il valore di op inject è proprio quello di essere il ponte. Quando introdurremo un secret manager cloud, cambierà solo questo step: i puntatori op://... nel template restano identici, i processi rimangono gli stessi. Nessun drift, nessuna migrazione traumatica.
È il classico "buono oggi, ottimo domani", il tipo di decisione architetturale che si sottovaluta quando si progetta pensando solo allo stato finale.
Chi legge lo script capisce l'intento senza aprire la documentazione della CLI.
Su altri OS non ho ancora verificato. Meglio esplicito che implicito quando si parla di permessi su file che contengono secrets in chiaro.
Cintura e bretelle. È un'abitudine che tengo in ogni script che tocca materiale sensibile.
op inject e op run, due filosofie diverse
A questo punto abbiamo incontrato entrambi i comandi e vale la pena fermarsi un attimo per capire la distinzione. La prima volta anche io li ho confusi: "fanno la stessa cosa, no?". In realtà no, e capire quando usare quale fa risparmiare parecchie ore di debug.
Un'analogia veloce:
op inject
op run
Stampa una lettera con i valori e la lascia sul tavolo
Sussurra i valori all'orecchio del destinatario
Produce un file .env.production con i valori in chiaro
Non produce nessun file
L'app legge il file come qualsiasi env file
L'app riceve le env var in memoria, direttamente dal processo padre
Scrive su disco. L'app non sa nemmeno che 1Password esiste: legge un .env standard.
op run --env-file=.env.template -- <comandodaeseguire>
└─────────────┬──────┘ └┬┘ └─────────┬─────────┘
template input sep. processo figlio
riceve le env varCode language:HTML, XML(xml)
Niente disco. Quando il processo figlio termina, le env var svaniscono con lui.
In pratica, quando si usa quale?
Se l'app gira come servizio long-running (systemd, Docker con ENTRYPOINT fisso) che si aspetta un .env: vai di op inject. È il caso del mio Strato 2.
Se stai lanciando un'app in dev, uno script one-shot, un job CI che finisce in pochi minuti: op run è più pulito. È il caso del mio Strato 1.
In tutti i casi in cui puoi mettere op run davanti al comando di avvio, fallo. Più sicuro, nessun file sensibile che può essere dimenticato da qualche parte.
Regola spicciola che ho adottato: op inject solo quando op run non è praticabile. Non per filosofia, per abitudine difensiva.
Con Business disponi di Service Account, e puoi usare l'action ufficiale 1password/load-secrets-action@v2 dentro il workflow:
- uses: 1password/load-secrets-action@v2
with:
export-env: true
env:
OP_SERVICE_ACCOUNT_TOKEN: ${{ secrets.OP_SERVICE_ACCOUNT_TOKEN }}
AWS_ACCESS_KEY_ID: op://DevOps-Core/AWS-Root/access_key_id
OPENAI_API_KEY: op://Company-AI-Tools/OpenAI/api_key
- run: ./deploy.sh # le env var sono già popolateCode language:PHP(php)
A quel punto in GitHub Secrets resta un solo valore: il token del Service Account. Tutto il resto è reference op://.... Zero duplicazione, zero sync manuale, rotazione automatica al prossimo run del workflow.
E ho verificato anche il flusso "Repository Secret in un workflow" con un test minimale:
Nei log dello step Echo length si vede DEMO_SECRET: *** (masking automatico di GitHub) e length=5 (prova che il secret è stato letto).
I gotcha che ho scoperto (e che non troverete nella doc ufficiale)
Questa è la parte per cui vale la pena leggere un articolo vs la documentazione di 1Password. I dettagli veri, dal campo.
1. op inject scansiona anche i commenti
Nel mio template avevo scritto un bel commento esplicativo:
# Questo file contiene solo reference op://, nessun valore reale.
DATABASE_URL=op://Boosha/project-demo/database_urlCode language:PHP(php)
Lancio op inject e mi ritrovo questo errore:
[ERROR] invalid secret reference 'op://': too few '/'Code language:JavaScript(javascript)
"Eh?". Ci ho messo dieci minuti a capire. La CLI scansiona l'intero file alla ricerca di stringhe op://..., inclusi i commenti #. La mia frase "reference op://, nessun valore reale" conteneva la stringa op:// seguita da virgola: per il parser è un puntatore malformato.
Regola: nei commenti dei template 1Password, non scrivere mai la stringa letterale op://. Rimandare alla documentazione se serve spiegare la sintassi.
2. GitHub maschera anche quello che non dovreste vedere
Nel workflow test-secret.yml non c'è nessun echo $DEMO_SECRET. Eppure nei log dello step compare DEMO_SECRET: ***.
Come mai? GitHub espande nei log la sezione env: di ogni step al setup, e lo fa già mascherando i valori noti come secrets. Conseguenza pratica: il classico pattern "difensivo" echo "length=${#VAR}" (che quasi tutti i tutorial raccomandano) con GitHub Actions è una buona abitudine ma non strettamente necessaria.
Su Jenkins o altri CI la storia è diversa: il masking va configurato, spesso male. Il pattern length=${#VAR} lì resta obbligatorio.
3. op read vuole il field name in inglese
Anche se la vostra app 1Password è in italiano e mostra "Chiave pubblica", la CLI vuole public key:
# funziona
op read "op://Personal/GitHub (Fedora Legion 2026)/public key"# NON funziona
op read "op://Personal/GitHub (Fedora Legion 2026)/chiave pubblica"Code language:PHP(php)
Classico mismatch tra UI localizzata e API. Mi ha fatto perdere 5 minuti la prima volta che dovevo estrarre una pubkey SSH per git config.
4. L'SSH Agent funziona, ma ssh-add -l dice di no
Setup con IdentityAgent ~/.1password/agent.sock in ~/.ssh/config. Apri il terminale, lanci ssh-add -l:
Could not open a connection to your authentication agent.
Panico iniziale. In realtà è normale: ssh-add -l interroga un socket in SSH_AUTH_SOCK, che con il setup IdentityAgent-only non è settato. Il test che conta è:
ssh -T git@github.com
# Hi <username>! You've successfully authenticated...Code language:PHP(php)
Funziona. È l'agente di 1Password che risponde, ma non espone il socket compatibile con ssh-add.
Difesa in profonditÃ
Il messaggio forte dell'esperienza di oggi è che non esiste un singolo layer che ti mette al sicuro. Serve accettare che ogni layer è imperfetto e costruire ridondanza:
Layer
Blocca
Se salta…
.gitignore + review di PR
.env
…il .env.staging con nome strano passa inosservato
secrets ad alta entropia, chiavi SSH, file .env committati
…il collega che ha dimenticato pre-commit install committa lo stesso
CI su GitHub Actions
tutto quello che i due sopra hanno mancato
…se anche la CI manca un pattern, almeno hai audit log GitHub per la forensic
Masking runtime (op run + GitHub Actions)
valori nei log di runtime
…c'è chi fa --no-masking per debug e dimentica di disabilitarlo
Nessuno dei layer è perfetto. Tutti insieme lo diventano abbastanza. È zero-trust applicato al filesystem, non un'architettura enterprise.
Nel mio smoke test ho verificato che tutti e quattro questi layer sono attivi di default con la configurazione di base che documento nel repo. Un dev nuovo che fa git clone + pre-commit install + op signin è già coperto, senza leggere un manuale di sicurezza di 40 pagine.
Cosa ho lasciato fuori (puntata 3?)
Onestà intellettuale: non è tutto rose e fiori sul piano Personal.
Due cose che oggi non ho potuto testare per davvero:
1password/load-secrets-action nei workflow GitHub. Richiede un Service Account. Quello che ho testato oggi (Repository Secrets copiati a mano) è il "day 0", non il target finale.
Ho fatto una variante dello smoke test adatta al Personal, con tutti gli step che simulano ma non replicano al 100% il flusso Business. È sufficiente per valutare: se la proposta verrà approvata e Boosha adotterà Business, ripeterò lo stesso percorso su un account di test e ne verrà fuori la terza puntata, con numeri reali di un deploy Docker, non simulazioni.
Nel frattempo, quello che ho documentato è abbastanza per partire. Lo scheletro resta identico, cambiano solo un paio di comandi.
Takeaway
Se devo portare a casa quattro cose da questo articolo:
1Password non è solo un password manager. È infrastruttura di gestione identità + secrets per team che non hanno (ancora) un Vault di HashiCorp o un Secrets Manager cloud. A €8/utente/mese, il ROI è banale.
Il modello è a tre strati. Dev locale = op run. Produzione = op inject. CI/CD = GitHub Secrets oggi, load-secrets-action domani. Stesso vault, stessa fonte umana, tre modi di consumare. Questa coerenza è il valore vero, non le singole feature.
Il pattern è pronto per il futuro. Il giorno in cui introdurremo AWS/GCP Secrets Manager, non dovremo rifare niente: i puntatori op://... restano, cambia solo chi li risolve. Decisione architetturale che paga.
Gli strumenti da soli non bastano, ma con 1Password la cosa giusta diventa la cosa più comoda. Questo è il motivo per cui penso che un team di 10-20 persone che adotta 1Password oggi si risparmia una notte storta tra sei mesi.
Io l'ho fatto: prima per testarlo sul mio setup, poi come proposta per il team di Boosha. È stato divertente e istruttivo, e ora non torno più indietro.
Se state valutando di portare 1Password nel vostro team e avete domande, scrivetemi: ho fatto gli errori per voi, e la scrittura su questi temi vive di feedback dal campo.
Hai mai desiderato che la tua AI potesse davvero fare qualcosa invece di limitarsi a parlarne? Bene, questo sogno è ora realtà grazie al Model Context Protocol (MCP). Cosa Ho Imparato nel Corso Zero to Mastery Ho appena completato il corso MCP di Diogo Resende su Zero to Mastery, e devo dire che è stata […]
Un percorso pratico tra privacy, apprendimento e qualche fallimento istruttivo Dopo soli 8 giorni dal Linux Day… io non riesco a stare ferma, ho ricevuto troppi input, troppe cose da sperimentare, e nuovi mondi in cui perdersi! (se ti sei perso il mio racconto, puoi recuperarlo qui!) TL;DR: Ho sostituito Google Calendar con Nextcloud self-hosted […]