Post

OpenBao in Docker: secret, AppRole e file .env con Agent

Installa OpenBao con Docker, Raft e TLS: crea ACL e AppRole via CLI e genera file .env e config.yaml per le applicazioni con OpenBao Agent.

OpenBao in Docker: secret, AppRole e file .env con Agent

Password nei file Compose, token dentro il repository e credenziali copiate a mano nei container sono comodi fino al giorno in cui bisogna ruotarli, revocarli o capire chi li ha letti. OpenBao centralizza questi secret, cifra i dati a riposo e decide quale identità possa accedere a ciascun percorso.

OpenBao non sostituisce SOPS e age: risolve un problema diverso. Se devi conservare file YAML, JSON o .env in un repository, trovi esempi pratici in Cripta informazioni sensibili prima di conservarle. SOPS e age cifrano i file prima del commit; OpenBao mantiene invece i secret fuori dal repository e li consegna a runtime applicando identità, policy e audit. Nei flussi più strutturati i due approcci possono convivere.

Cos’è OpenBao

OpenBao Progetto OpenSSF Open source Secrets management Gestore open source per secret, credenziali dinamiche, cifratura e PKI, con policy ACL, autenticazione dei workload e audit centralizzato.

OpenBao è un sistema open source di gestione dei secret e della cifratura basato sull’identità. Un utente o un’applicazione si autentica, riceve un token associato a una o più policy e può eseguire soltanto le operazioni permesse su determinati path. Il progetto è ospitato da OpenSSF e distribuito con licenza MPL 2.0.

In questa guida costruiremo un esempio completo:

  • un server OpenBao in Docker con TLS e storage Raft persistente;
  • un audit log su un volume protetto;
  • un motore KV v2 con i secret di una piccola applicazione;
  • una policy ACL in sola lettura;
  • un’AppRole configurata interamente via CLI;
  • OpenBao Agent, che autentica il workload e genera sia .env sia config.yaml;
  • un entrypoint che attende i file, importa solo le variabili consentite e avvia il processo principale.

Gli esempi sono stati verificati sulla release OpenBao 2.6.1. Il tag dell’immagine è fissato intenzionalmente: aggiornatelo soltanto dopo aver letto le note di rilascio e provato backup e ripristino.

Architettura e limiti

Il flusso sarà questo:

Architettura OpenBao: amministrazione via TLS, storage Raft, audit, Agent con AppRole e file temporanei letti dall'applicazione

Diagramma generato con Graphviz: l’applicazione legge soltanto i file prodotti da Agent e non comunica direttamente con OpenBao.

La porta API viene pubblicata soltanto su 127.0.0.1; Agent parla con OpenBao sulla rete Docker privata, mentre l’app non ha accesso diretto al server dei secret. I file generati passano attraverso /run, normalmente una tmpfs sui sistemi Linux con systemd.

Questo è un deployment single-node: è persistente e adatto a un laboratorio o a un piccolo homelab, ma non è alta affidabilità. In produzione servono almeno tre nodi Raft distribuiti, snapshot provati, monitoraggio e un meccanismo di auto-unseal basato su KMS o HSM.

Chi controlla il Docker host come root può leggere i bind mount, ispezionare i processi e sostituire i container. OpenBao protegge dalla dispersione dei secret e applica autorizzazioni e audit, ma non rende affidabile un host già compromesso.

Prerequisiti

Servono un host Linux aggiornato, Docker Engine con il plugin Compose e OpenSSL. Riserviamo identità e gruppi numerici distinti:

  • 2000:2000 per il server OpenBao;
  • UID 2100 per Agent;
  • UID 2200 per l’applicazione dimostrativa;
  • GID 3100, chiamato openbao-demo-read e condiviso soltanto da Agent e demo-app per leggere i file renderizzati.

Sono UID e GID di esempio: controllate con getent passwd 2000, getent passwd 2100, getent passwd 2200 e getent group 3100 che non siano già utilizzati. I nomi nel database utenti dell’host non sono obbligatori per Docker, ma documentarli evita collisioni e rende più leggibili audit e backup. Possiamo dare un nome al gruppo sul solo host, senza aggiungervi utenti umani:

1
sudo groupadd --system --gid 3100 openbao-demo-read

I container continuano a usare il GID numerico, quindi non dipendono dal file /etc/group dell’host.

Prepariamo la struttura:

1
2
3
4
5
mkdir -p \
  server/{config,tls,data,audit} \
  bootstrap/policies \
  agent/{config,templates,auth} \
  app scripts

Il risultato sarà simile a questo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
|-- compose.yaml
|-- server
|   |-- config/bao.hcl
|   |-- tls/{ca.crt,server.crt,server.key}
|   |-- data/
|   `-- audit/
|-- bootstrap/policies/demo-app.hcl
|-- agent
|   |-- config/agent.hcl
|   |-- templates/{app.env.ctmpl,config.yaml.ctmpl}
|   `-- auth/{role_id,secret_id}
|-- app/openbao-entrypoint.sh
`-- scripts/issue-agent-credentials.sh

Configurare il server

Creiamo server/config/bao.hcl:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
ui           = true
api_addr     = "https://openbao:8200"
cluster_addr = "https://openbao:8201"

storage "raft" {
  path    = "/openbao/data"
  node_id = "openbao-1"
}

listener "tcp" {
  address         = "0.0.0.0:8200"
  cluster_address = "0.0.0.0:8201"

  tls_cert_file   = "/openbao/tls/server.crt"
  tls_key_file    = "/openbao/tls/server.key"
  tls_min_version = "tls12"
}

api_addr deve diventare il vero FQDN raggiungibile dai client quando il server esce dalla singola rete Compose. Anche quel nome dovrà comparire nei SAN del certificato.

Certificato TLS per il laboratorio

Per rendere l’esempio riproducibile generiamo un certificato autofirmato valido per il nome Docker openbao e per l’accesso locale:

1
2
3
4
5
6
7
8
9
10
openssl req -x509 -newkey rsa:4096 -sha256 -days 365 -nodes \
  -keyout server/tls/server.key \
  -out server/tls/server.crt \
  -subj "/CN=openbao" \
  -addext "subjectAltName=DNS:openbao,IP:127.0.0.1" \
  -addext "basicConstraints=critical,CA:TRUE" \
  -addext "keyUsage=critical,digitalSignature,keyEncipherment,keyCertSign" \
  -addext "extendedKeyUsage=serverAuth"

cp server/tls/server.crt server/tls/ca.crt

Questo certificato è accettabile solo per un laboratorio. In produzione usate una CA interna o pubblica, montate la sola catena CA insieme al certificato e alla chiave del server e non conservate mai la chiave privata della CA sul nodo OpenBao.

Definire la policy ACL

OpenBao nega implicitamente ciò che una policy non autorizza. Il file bootstrap/policies/demo-app.hcl concede la lettura di un solo secret KV v2 e le operazioni necessarie ad Agent per controllare e rinnovare il proprio token:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
path "kv/data/apps/demo" {
  capabilities = ["read"]
}

path "auth/token/lookup-self" {
  capabilities = ["read"]
}

path "auth/token/renew-self" {
  capabilities = ["update"]
}

path "auth/token/revoke-self" {
  capabilities = ["update"]
}

Con KV v2 la policy usa il path API kv/data/..., non il percorso abbreviato mostrato dai comandi bao kv. Non concediamo list, create, update o accesso agli altri secret.

Configurare OpenBao Agent

Il file agent/config/agent.hcl configura Agent. Il config.yaml che vedremo dopo è invece un file dell’applicazione generato da Agent: sono due ruoli distinti.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
pid_file = "/tmp/openbao-agent.pid"

vault {
  address = "https://openbao:8200"
  ca_cert = "/openbao/tls/ca.crt"
}

auto_auth {
  method {
    type       = "approle"
    mount_path = "auth/approle"

    config = {
      role_id_file_path                  = "/openbao/auth/role_id"
      secret_id_file_path                = "/openbao/auth/secret_id"
      secret_id_response_wrapping_path   = "auth/approle/role/demo-app/secret-id"
      remove_secret_id_file_after_reading = true
    }
  }
}

template_config {
  exit_on_retry_failure          = true
  static_secret_render_interval = "5m"
}

template {
  source               = "/openbao/templates/app.env.ctmpl"
  destination          = "/run/openbao/app.env"
  perms                = "0640"
  backup               = false
  error_on_missing_key = true
}

template {
  source               = "/openbao/templates/config.yaml.ctmpl"
  destination          = "/run/openbao/config.yaml"
  perms                = "0640"
  backup               = false
  error_on_missing_key = true
}

Il blocco si chiama ancora vault per compatibilità del formato. Non aggiungiamo un token sink: Agent usa il token in memoria esclusivamente per i template. backup = false evita di lasciare sul disco la versione precedente dei secret.

Template per .env

Creiamo agent/templates/app.env.ctmpl:

1
2
3
4
5
{{- with secret "kv/data/apps/demo" -}}
DB_USERNAME={{ .Data.data.db_username }}
DB_PASSWORD={{ .Data.data.db_password }}
API_TOKEN={{ .Data.data.api_token }}
{{- end }}

Questo formato va bene per valori su una sola riga. Certificati PEM, chiavi o secret multilinea devono essere renderizzati in file separati, non forzati in un .env.

Template per config.yaml

Creiamo agent/templates/config.yaml.ctmpl:

1
2
3
4
5
6
7
8
9
10
{{- with secret "kv/data/apps/demo" -}}
database:
  host: {{ .Data.data.db_host | toJSON }}
  port: {{ .Data.data.db_port | toJSON }}
  username: {{ .Data.data.db_username | toJSON }}
  password: {{ .Data.data.db_password | toJSON }}
application:
  api_token: {{ .Data.data.api_token | toJSON }}
  log_level: {{ .Data.data.log_level | toJSON }}
{{- end }}

toJSON produce stringhe correttamente quotate e sottoposte a escaping; JSON è un sottoinsieme valido di YAML. Nell’esempio generiamo entrambi i formati per mostrarne l’uso, ma un’applicazione reale dovrebbe preferire il solo formato che sa ricaricare correttamente.

Creare un entrypoint sicuro

Il file app/openbao-entrypoint.sh attende i template e importa soltanto tre nomi ammessi. Non usa sourceeval: un valore contenente $() o backtick rimane una stringa e non diventa un comando.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#!/bin/sh
set -eu

secrets_dir=${OPENBAO_SECRETS_DIR:-/run/openbao}
env_file="$secrets_dir/app.env"
config_file="$secrets_dir/config.yaml"

attempt=0
while [ ! -s "$env_file" ] || [ ! -s "$config_file" ]; do
  attempt=$((attempt + 1))
  if [ "$attempt" -ge 60 ]; then
    echo "OpenBao Agent non ha generato i file entro 60 secondi" >&2
    exit 1
  fi
  sleep 1
done

while IFS= read -r line || [ -n "$line" ]; do
  case "$line" in
    ''|'#'*)
      continue
      ;;
    DB_USERNAME=*|DB_PASSWORD=*|API_TOKEN=*)
      export "$line"
      ;;
    *)
      echo "Variabile non autorizzata nel file .env" >&2
      exit 1
      ;;
  esac
done < "$env_file"

export APP_CONFIG_FILE="$config_file"
exec "$@"

exec fa diventare l’applicazione il processo principale e le consente di ricevere correttamente i segnali di stop. Nell’immagine reale conviene copiare questo script durante la build, con proprietà root:root e modalità 0555, invece di montarlo dall’host.

Docker Compose

Ora creiamo compose.yaml:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
services:
  openbao:
    image: openbao/openbao:2.6.1
    user: "2000:2000"
    command: ["server"]
    environment:
      HOME: /tmp
      SKIP_CHOWN: "1"
      BAO_ADDR: https://127.0.0.1:8200
      BAO_CACERT: /openbao/tls/ca.crt
    ports:
      - "127.0.0.1:8200:8200"
    volumes:
      - ./server/config:/openbao/config:ro
      - ./server/tls:/openbao/tls:ro
      - ./server/data:/openbao/data
      - ./server/audit:/openbao/audit
      - ./bootstrap/policies:/openbao/bootstrap/policies:ro
    tmpfs:
      - /tmp:size=16m,mode=1777
      - /openbao/logs:size=1m,uid=2000,gid=2000,mode=0700
      - /openbao/file:size=1m,uid=2000,gid=2000,mode=0700
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    mem_limit: 512m
    memswap_limit: 512m
    mem_swappiness: 0
    networks:
      - bao
    restart: unless-stopped

  openbao-agent:
    image: openbao/openbao:2.6.1
    user: "2100:3100"
    command: ["agent", "-config=/openbao/agent/agent.hcl"]
    environment:
      HOME: /tmp
      SKIP_CHOWN: "1"
    volumes:
      - ./agent/config:/openbao/agent:ro
      - ./agent/templates:/openbao/templates:ro
      - ./agent/auth:/openbao/auth
      - ./server/tls/ca.crt:/openbao/tls/ca.crt:ro
      - /run/openbao/demo:/run/openbao
    tmpfs:
      - /tmp:size=16m,mode=1777
      - /openbao/logs:size=1m,uid=2100,gid=3100,mode=0700
      - /openbao/file:size=1m,uid=2100,gid=3100,mode=0700
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    mem_limit: 128m
    memswap_limit: 128m
    mem_swappiness: 0
    depends_on:
      - openbao
    networks:
      - bao
    restart: unless-stopped

  demo-app:
    image: alpine:3.22
    user: "2200:3100"
    entrypoint: ["/usr/local/bin/openbao-entrypoint.sh"]
    command: ["tail", "-f", "/dev/null"]
    environment:
      OPENBAO_SECRETS_DIR: /run/openbao
    volumes:
      - ./app/openbao-entrypoint.sh:/usr/local/bin/openbao-entrypoint.sh:ro
      - /run/openbao/demo:/run/openbao:ro
    tmpfs:
      - /tmp:size=16m,mode=1777
    read_only: true
    cap_drop:
      - ALL
    security_opt:
      - no-new-privileges:true
    mem_limit: 128m
    memswap_limit: 128m
    mem_swappiness: 0
    depends_on:
      - openbao-agent
    networks:
      - app
    restart: unless-stopped

networks:
  bao:
    internal: true
  app:
    internal: true

L’immagine alpine simula un processo applicativo e non legge i secret: va sostituita con la vostra immagine. depends_on ordina l’avvio, ma non garantisce che l’autenticazione sia conclusa; il ciclo di attesa nell’entrypoint gestisce proprio questa condizione.

OpenBao ha rimosso mlock dalla versione 2.0. Per ridurre lo swap dei dati sensibili usiamo limiti cgroup con memoria e swap uguali, oltre a mem_swappiness: 0. Verificate che il kernel abbia il supporto agli swap limit e adattate i valori alla dimensione reale del database.

Chi deve possedere i secret

La separazione importante non è soltanto tra host e container, ma tra credenziali di autenticazione e secret già renderizzati:

Percorso o datoProprietarioGruppoModoChi lo riceve
server/data e server/audit200020000700solo il server OpenBao
server.keyamministratore host20000640solo il server OpenBao
agent/auth/role_id210031000400solo Agent
agent/auth/secret_id210031000400solo Agent, poi viene eliminato
/run/openbao/demo210031000750Agent scrive, demo-app attraversa
.env e config.yaml210031000640Agent scrive, demo-app legge

Il token AppRole esiste nella memoria di Agent e non viene scritto in un sink. L’applicazione gira come 2200:3100: grazie al gruppo può leggere i file, ma il bind mount è anche read-only, quindi non può modificarli. La directory agent/auth non viene montata nell’app, che non deve mai ricevere RoleID, SecretID o token OpenBao.

Il gruppo openbao-demo-read (3100) è una capacità di lettura dei secret di demo-app. Non assegnatelo a utenti o container generici e non aggiungete l’app ai gruppi 2000, docker o a quelli riservati ad altri workload.

Applicare i permessi

Prima dell’avvio rendiamo esplicita la proprietà di ogni percorso:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
sudo chown -R "$USER":2000 server/config server/tls bootstrap/policies
sudo chmod 0750 server/config server/tls bootstrap/policies
sudo chmod 0640 server/config/bao.hcl server/tls/server.key \
  bootstrap/policies/demo-app.hcl
sudo chmod 0644 server/tls/server.crt server/tls/ca.crt

sudo chown -R 2000:2000 server/data server/audit
sudo chmod 0700 server/data server/audit

sudo chown -R "$USER":3100 agent/config agent/templates
sudo chmod 0750 agent/config agent/templates
sudo chmod 0640 agent/config/agent.hcl agent/templates/*.ctmpl

sudo chown 2100:3100 agent/auth
sudo chmod 0700 agent/auth
chmod 0555 app/openbao-entrypoint.sh

I file renderizzati contengono secret in chiaro. Creiamo quindi la directory condivisa in /run, che sui normali host Linux è volatile:

1
2
sudo install -d -o root -g root -m 0711 /run/openbao
sudo install -d -o 2100 -g 3100 -m 0750 /run/openbao/demo

Il parent 0711 è attraversabile conoscendo il nome del path ma non elencabile dagli utenti normali. La directory dell’app consente a UID 2100 di scrivere e al solo gruppo 3100 di attraversare e leggere.

Per ricrearla automaticamente dopo ogni riavvio, aggiungiamo /etc/tmpfiles.d/openbao-apps.conf:

1
2
d /run/openbao      0711 root root -
d /run/openbao/demo 0750 2100 3100 -

e applichiamo la regola:

1
sudo systemd-tmpfiles --create /etc/tmpfiles.d/openbao-apps.conf

Aggiungere altre applicazioni

Ogni nuovo confine di fiducia dovrebbe avere una policy, un’AppRole, un Agent, un UID e un gruppo propri. Per esempio:

WorkloadUID AgentUID appGID letturaDirectory runtime
demo-app210022003100/run/openbao/demo
billing-app210122013101/run/openbao/billing
monitoring-app210222023102/run/openbao/monitoring

La seconda app userà quindi una policy come billing-app, un’AppRole con lo stesso nome, un servizio openbao-agent-billing eseguito come 2101:3101 e un’app eseguita come 2201:3101. La sua directory sarà 0750 e i file 0640. In questo modo né demo-app né il suo Agent possono attraversare il path di billing-app, anche se i container condividono lo stesso host.

I gruppi possono chiamarsi openbao-demo-read, openbao-billing-read e openbao-monitoring-read. Il suffisso -read ricorda che concedono accesso ai secret già renderizzati, non al server OpenBao né alle credenziali AppRole.

Un singolo Agent può renderizzare più template, ma così diventa una trust boundary comune: se le app non devono condividere i secret, è più coerente usare un Agent e un’AppRole per ciascuna applicazione.

Nel repository del deployment ignoriamo almeno questi percorsi:

server/data/
server/audit/
server/tls/
agent/auth/

Policy, template e configurazioni possono invece essere versionati perché non contengono credenziali.

Avviare, inizializzare e fare unseal

Validiamo Compose e avviamo soltanto il server:

1
2
3
4
docker compose config
docker compose pull openbao
docker compose up -d openbao
docker compose logs openbao

Non usiamo mai server -dev: la modalità dev mantiene i dati in memoria, disabilita protezioni importanti e crea automaticamente un root token noto.

Inizializziamo il barrier con cinque quote e una soglia di tre:

1
2
docker compose exec openbao \
  bao operator init -key-shares=5 -key-threshold=3

Distribuite le cinque unseal key a custodi distinti e conservate il root token iniziale in un password manager o in un supporto cifrato. Per un ambiente serio è preferibile cifrare già l’output di operator init con le chiavi PGP dei custodi.

Inseriamo tre quote, una alla volta e senza passarle come argomenti visibili:

1
2
3
4
docker compose exec openbao bao operator unseal
docker compose exec openbao bao operator unseal
docker compose exec openbao bao operator unseal
docker compose exec openbao bao status

Dopo un riavvio il nodo resta inizializzato ma torna sealed. L’auto-unseal evita questa procedura manuale, ma sposta la fiducia su un KMS o un HSM e va progettato insieme al piano di disaster recovery.

Amministrare OpenBao dalla CLI

Per il bootstrap usiamo il root token senza salvarlo nel token helper del client:

1
2
3
4
5
6
7
read -rsp 'Root token: ' BAO_TOKEN
export BAO_TOKEN
printf '\n'

baoctl() {
  docker compose exec -T -e BAO_TOKEN openbao bao "$@"
}

Abilitiamo subito l’audit file:

1
2
3
baoctl audit enable file \
  file_path=/openbao/audit/audit.log \
  mode=0600

Anche l’audit log è sensibile: contiene metadati, identità e path richiesti. Va raccolto, ruotato e protetto con la stessa attenzione degli altri log di sicurezza.

Abilitare KV v2 e inserire i secret

1
baoctl secrets enable -path=kv kv-v2

Per non lasciare password nella cronologia o nella lista dei processi, creiamo fuori dal repository /tmp/openbao-demo-secrets.json con umask 077:

1
2
3
4
5
6
7
8
{
  "db_host": "postgres",
  "db_port": 5432,
  "db_username": "demo",
  "db_password": "SOSTITUIRE_CON_UN_VALORE_FORTE",
  "api_token": "SOSTITUIRE_CON_UN_TOKEN_REALE",
  "log_level": "info"
}

Poi inviamo il JSON tramite standard input:

1
2
3
umask 077
baoctl kv put -mount=kv apps/demo - \
  < /tmp/openbao-demo-secrets.json

Eliminate il file temporaneo appena verificato il caricamento. Su filesystem copy-on-write o SSD la cancellazione sicura non è garantita: per workflow ricorrenti è meglio produrre il JSON direttamente da un password manager o da un canale cifrato e passarlo in pipe.

Caricare la policy

1
2
3
4
5
baoctl policy write \
  demo-app \
  /openbao/bootstrap/policies/demo-app.hcl

baoctl policy read demo-app

Creare l’AppRole

Abilitiamo il metodo e creiamo un ruolo per il solo workload demo-app:

1
2
3
4
5
6
7
8
9
10
baoctl auth enable approle

baoctl write auth/approle/role/demo-app \
  bind_secret_id=true \
  token_policies=demo-app \
  token_no_default_policy=true \
  token_type=service \
  token_period=20m \
  secret_id_ttl=10m \
  secret_id_num_uses=1

Il token periodico viene rinnovato da Agent ogni volta che il processo resta in esecuzione; non ha quindi bisogno di un SecretID riutilizzabile. Una revoca lo interrompe immediatamente. Il RoleID identifica il ruolo e non è trattato come una password, mentre il SecretID è una credenziale sensibile, monouso e valida solo dieci minuti.

Consegnare il SecretID con response wrapping

Non mettiamo il SecretID nel Compose, in un’immagine o in Git. OpenBao genera invece un wrapping token con TTL di cinque minuti; Agent lo apre, controlla che provenga dall’endpoint previsto, usa il SecretID e cancella il file.

Creiamo scripts/issue-agent-credentials.sh:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#!/bin/sh
set -eu

: "${BAO_TOKEN:?Esporta prima un token amministrativo in BAO_TOKEN}"

project_dir=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$project_dir"

umask 077
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT HUP INT TERM

bao() {
  docker compose exec -T -e BAO_TOKEN openbao bao "$@"
}

bao read -field=role_id \
  auth/approle/role/demo-app/role-id \
  > "$tmp_dir/role_id"

bao write -wrap-ttl=5m -field=wrapping_token -f \
  auth/approle/role/demo-app/secret-id \
  > "$tmp_dir/secret_id"

sudo install -o 2100 -g 3100 -m 0400 \
  "$tmp_dir/role_id" agent/auth/role_id
sudo install -o 2100 -g 3100 -m 0400 \
  "$tmp_dir/secret_id" agent/auth/secret_id

echo "Credenziali AppRole consegnate senza mostrarne il contenuto."

Eseguiamolo:

1
2
chmod 0750 scripts/issue-agent-credentials.sh
./scripts/issue-agent-credentials.sh

Il wrapping token è anch’esso un bearer secret, ma è monouso, scade rapidamente e non rivela il SecretID a chi lo trasporta. Se Agent viene riavviato, occorre eseguire nuovamente lo script prima della partenza: il SecretID precedente è già stato consumato e il relativo file viene eliminato.

In un orchestratore è preferibile usare l’identità nativa del workload, per esempio Kubernetes Auth o JWT/OIDC, invece di distribuire AppRole. Qui AppRole è adatta perché stiamo lavorando con normali container su un singolo host.

Avviare Agent e l’applicazione

1
2
3
docker compose pull openbao-agent demo-app
docker compose up -d openbao-agent demo-app
docker compose logs -f openbao-agent

Controlliamo i permessi senza stampare alcun valore:

1
2
3
4
5
sudo stat -c '%A %u:%g %n' /run/openbao/demo/*

docker compose exec demo-app sh -c \
  'test -r /run/openbao/config.yaml && \
   tr "\0" "\n" < /proc/1/environ | grep -q "^DB_USERNAME="'

I due file devono appartenere a 2100:3100 ed essere 0640. L’app gira con UID 2200, li legge grazie al solo GID 3100 e non possiede né il RoleID né il wrapping token.

Quando un valore KV cambia, Agent rigenera i template entro l’intervallo static_secret_render_interval. Il config.yaml può essere riletto da un’applicazione che supporta il reload; le variabili d’ambiente di un processo già avviato, invece, non cambiano. Per applicare il nuovo .env bisogna riavviare soltanto l’app, oppure usare il Process Supervisor di OpenBao Agent e progettare esplicitamente il ciclo di riavvio.

Terminato il bootstrap:

1
unset BAO_TOKEN

Configurate subito un metodo di autenticazione nominativo per gli amministratori e verificate che funzioni prima di revocare il root token iniziale. Un root token non dovrebbe diventare la credenziale usata ogni giorno.

Cosa manca per la produzione

Prima di affidare a questo servizio credenziali reali, aggiungerei almeno:

  1. tre o cinque nodi Raft su domini di guasto differenti;
  2. snapshot periodici con bao operator raft snapshot save e prove di restore;
  3. auto-unseal tramite KMS/HSM e custodia separata delle recovery key;
  4. autenticazione OIDC o certificati client per gli amministratori;
  5. raccolta remota degli audit log, metriche e alert su seal, errori e scadenze;
  6. policy separate per ogni applicazione e ambiente;
  7. rotazione dei secret e, dove possibile, credenziali database dinamiche;
  8. accesso alla porta 8200 limitato da firewall, VPN o rete di management.

OpenBao risolve soprattutto il problema dell’identità: non più “chi possiede questo file può leggere tutto”, ma “questa applicazione autenticata può leggere questo path e nient’altro”. Agent completa il disegno tenendo token e logica di autenticazione fuori dal codice applicativo. I file .env restano secret in chiaro, ma diventano artefatti temporanei, protetti e rigenerabili invece di credenziali permanenti sparse nei repository.

Fonti ufficiali

Questo post è sotto licenza CC BY 4.0 a nome dell'autore.