Policy in Rego#
Con governance.engine: opa le regole di governance si scrivono in Rego e le valuta OPA incorporato in manent: niente sidecar, nessuna chiamata di rete. La policy si può cambiare a runtime. Una nuova revisione viene caricata, testata, misurata sui deploy reali e approvata da un'altra persona, e ogni processo la riprende in pochi secondi, senza riavvii.
Le regole di manent.yml continuano a funzionare: senza una revisione, la policy predefinita le applica invariate (governance.rules diventano i dati data.manent.rules).
governance:
engine: opa # builtin (default) | opa
refresh: 10s # ogni quanto ogni processo rilegge policy e freeze a runtime
max_staleness: 10m # oltre questo senza una lettura riuscita, i deploy reali sono rifiutati
start: deny # deny | strict: cosa ricontrolla l'avvio del deploy (vedi sotto)
opa:
source: store # store (default) | file | bundle | consul
store:
review: true # approvazione da un'identità diversa dall'autore (quattro occhi)
tests: run # run (default) | required: revisioni senza test rifiutate
# query: data.manent.governance.decision
# eval_timeout: 200ms
rules: [...] # dati della policy predefinita
freezes: [...]
La decisione#
La policy riceve un input che descrive il deploy e restituisce una decisione: divieti e obblighi. manent fa rispettare gli obblighi come con le regole YAML. La policy dice cosa serve e manent lo verifica: lo stesso artefatto già rilasciato negli ambienti di requires, le approvazioni, il ticket di change.
Input#
{
"schema": 3,
"phase": "submit",
"now": "2026-10-18T09:30:00Z",
"application": "shop",
"environment": "prod",
"region": "",
"deployment": {"id": "ci-42", "kind": "deploy", "source": "", "dry_run": false, "schedule": {},
"freeze_override": "", "after": [], "release": ""},
"artifact": {"name": "shop", "version": "1.4.0", "type": "application", "sha256": "…", "source": "oci://…"},
"requested_by": {"id": "oidc:alice", "method": "oidc", "name": "alice", "groups": ["team-shop"]},
"plugin": "kubernetes",
"rollout": {"strategy": "waves", "waves": 3, "regions": ["eu", "us"], "approval_points": 1},
"targets": [{"host": "k8s-eu", "labels": {"region": "eu"}}],
"current": {"id": "…", "version": "1.3.2", "sha256": "…", "deployed_at": "2026-10-10T12:00:00Z"},
"freezes": {"active": [{"name": "year-end", "end": "2027-01-07T00:00:00+01:00", "hard": false}]}
}
| Campo | Note |
|---|---|
phase |
submit (accodamento), validate, start (avvio del deploy sul worker), stage (inizio di uno stadio del rollout, una volta per region), tick (esecuzione di una schedule ricorrente). |
now |
L'istante della valutazione: l'accodamento (o schedule.at), oppure il tempo letto dallo step che valuta. Le policy usano questo campo, perché l'orologio non è disponibile. |
deployment.kind |
deploy, promotion, rollback. |
targets, plugin, rollout |
Dal deploy effettivo (configurazione di ambiente fusa), quando è noto. |
current |
Release in esercizio nell'ambiente; assente al primo rilascio. |
freezes.active |
Finestre attive a now, calcolate da manent. |
tenant, application_tags |
Il tenant della release (name, tags; untenanted per le applicazioni che nessun tenant elenca, anche senza registro) e i tag dell'applicazione nel registro. Schema 3 (lo schema 2 aveva il tenant come cliente e tenants_run). |
L'input non contiene segreti né parametri dei plugin. Lo schema è versionato (schema): i campi si aggiungono e non vengono mai rinominati.
Decisione#
La query data.manent.governance.decision deve produrre sempre un oggetto (si dichiara un default): una decisione indefinita rifiuta il deploy.
| Chiave | Descrizione |
|---|---|
rule |
Nome descrittivo, mostrato nello stato e da validate. |
deny |
Insieme di {"code": …, "message": …} (o di stringhe): un insieme non vuoto rifiuta il deploy con i messaggi. |
requires |
Ambienti in cui lo stesso artefatto deve essere già stato rilasciato. |
approvals |
required, allow_self, timeout ("72h"), when (before/rollout) e approvers (groups, subjects): chi può approvare, oltre al permesso deployment.approve. null o required: 0: nessuna approvazione. |
change |
approval, timeout: ticket di change nell'ITSM (change management). |
freeze_exempt |
Nomi delle finestre attive che la policy esenta. Una policy può solo esentare: le finestre attive bloccano per default, e quelle hard non sono mai esentate. |
notes |
Spiegazioni mostrate da validate, dalla CLI e dall'interfaccia web. |
Chiavi sconosciute, tipi errati e durate non valide sono errori.
Esempio#
package manent.governance
import rego.v1
import data.manent.lib
prod if lib.match_any(["prod", "prod-*"], input.environment)
requires contains "preprod" if {
prod
input.deployment.kind != "rollback" # a rollback must never wait for preprod
}
default approvals := null
approvals := {"required": 2, "timeout": "72h", "approvers": {"groups": ["dba"]}} if {
prod
input.artifact.type == "migration"
} else := {"required": 1} if prod
default change := null
# Out of business hours, the change approved in the ITSM too.
change := {"approval": true, "timeout": "24h"} if {
prod
not lib.business_hours(input.now, "Europe/Rome")
}
# Hotfixes with an incident pass the weekend freeze (never a hard one).
freeze_exempt contains "weekend" if {
endswith(input.artifact.version, "-hotfix")
startswith(input.deployment.freeze_override, "INC-")
}
deny contains {"code": "downgrade", "message": "downgrades in production go through manent rollback"} if {
prod
input.deployment.kind == "deploy"
lib.semver_less(input.artifact.version, input.current.version)
}
decision := {
"rule": "custom",
"deny": deny,
"requires": requires,
"approvals": approvals,
"change": change,
"freeze_exempt": freeze_exempt,
}
Libreria e restrizioni#
import data.manent.lib mette a disposizione funzioni deterministiche:
| Funzione | Descrizione |
|---|---|
match_any(patterns, value) |
Pattern con * come in manent.yml (glob.match di OPA ha un'altra sintassi). |
semver_less(a, b) |
Versioni confrontate per numero; prefisso v ammesso, una pre-release precede la sua release. |
local_time(t, tz) |
year, month, day, weekday (0 = domenica), hour, minute di un istante in un fuso orario. |
business_hours(t, tz) |
Dalle 9:00 alle 18:00, dal lunedì al venerdì. |
tenant_matches(selettore, tenant) |
Un selettore di tenant su input.tenant ({} senza tenant: mai). |
app_match_any(pattern, input) |
Pattern qualificati delle applicazioni (payments/*, */*; senza prefisso solo untenanted) sul tenant e l'applicazione dell'input, come le regole di manent.yml. |
app_matches(selettore, input) |
Un selettore sull'applicazione dell'input: i suoi tag, application, tenant, tenant.<tag>. |
duration_ns(d) |
Una durata ("72h") in nanosecondi. |
data.manent.lib.rules implementa la semantica delle regole YAML su data.manent.rules: una policy può delegargli i casi che non gestisce (decision := rules.decision if not mine).
Una valutazione dipende solo da input e dati, quindi lo stesso input con la stessa revisione dà sempre la stessa decisione:
- non compilano i built-in che leggono l'orologio, la rete o il runtime (
time.now_ns,http.send,net.lookup_ip_addr,rand.intn,uuid.rfc4122,opa.runtime,tracee gli altri non deterministici); - Rego v1 in modalità strict;
- ogni valutazione dura al più
eval_timeout(default 200ms); - i package
manent.libsono riservati.
Dati#
Una revisione può contenere dati: data.json o data.yaml nella directory a/b/ finiscono sotto data.a.b. Due percorsi sono speciali:
data.manent.rules: regole con lo schema digovernance.rules(durate come stringhe). Se la revisione non le definisce, si usano quelle dimanent.yml.data.manent.freezes: finestre di freeze con lo schema digovernance.freezes(piùhard), per le sorgenti diverse dallo store (file, bundle, Consul), versionate insieme alla policy.
Test#
I moduli *_test.rego sono i test della revisione (opa test, con with input as …). Girano a ogni caricamento e un test fallito fa rifiutare la revisione. Con store.tests: required viene rifiutata anche una revisione senza test. Ogni revisione deve inoltre produrre una decisione valida su un insieme di input di esempio, uno per ogni fase e tipo di deploy.
manent-server policy test -d policies/ # compila e testa in locale, senza server
manent-server -c manent.yml policy eval -d policies/ -f deploy.yml [--phase start] [--kind rollback]
manent-server -c manent.yml policy export-rbac -d policies/ # binding di manent.yml come dati, rbac.engine: opa
Con -c le regole della configurazione diventano data.manent.rules, come sul server.
Revisioni gestite da manent#
Con source: store (default) le revisioni stanno nel database di manent e si gestiscono con la CLI, l'API o l'interfaccia web (pagina Policy).
manent policy push -d policies/ -m "approvazione dei DBA per le migrazioni" # bozza: compilata e testata dal server
manent policy diff 12 13 # differenze tra due revisioni
manent policy impact 13 --since 30d # decisioni dei deploy reali ricalcolate
manent policy eval 13 -f deploy.yml # decisione su un descrittore
manent policy approve 13 # attivazione, da un'altra identità con review: true
manent policy activate 12 # ritorno a una revisione già attiva in passato
manent policy list | show 13 | pull 13 -d out/
- Prima bozza, poi attiva. Una revisione caricata è una bozza e vale solo dopo l'approvazione. Con
store.review: truel'autore non può approvarla (403). - Impatto.
impactricostruisce gli input dei deploy del periodo dalle richieste registrate (connowuguale al loro accodamento) e confronta gli obblighi registrati allora con quelli della revisione. Così si vede prima dell'attivazione una revisione che bloccherebbe tutto o toglierebbe approvazioni. - Immutabilità. Una revisione non cambia mai: una correzione è una nuova revisione. La identifica lo sha256 dei suoi file.
- Propagazione. Ogni processo rilegge la revisione attiva ogni
refresh; su Postgres una notifica (LISTEN/NOTIFY) la porta in meno di un secondo. Una revisione non valida non sostituisce mai quella in vigore. - Ritorno.
activateriporta a qualsiasi revisione già stata attiva. Per tornare alle regole YAML si carica una revisione con il solodata.jsoncontenente{}.
Senza alcuna revisione attiva, la policy predefinita applica le regole di manent.yml.
Altre sorgenti#
| Sorgente | Configurazione | Quando si rilegge |
|---|---|---|
file |
opa.file.dir: file *.rego e di dati, file nascosti esclusi (ad esempio una ConfigMap di Kubernetes) |
cambiano nomi, dimensioni o date di modifica |
bundle |
opa.bundle.url (bundle OPA, tar.gz), ca_file, credentials_ref (bearer token), signing.public_key_ref, algorithm (default RS256), key_id, scope |
cambia l'ETag (If-None-Match) |
consul |
opa.consul.prefix (default manent/policies): ogni chiave sotto il prefisso è un file; indirizzo e token di opa.consul.address/token_file, altrimenti del primo provider consul di envconfig.providers |
cambia l'indice del prefisso |
La firma del bundle (.signatures.json, come la produce opa build --signing-key) è obbligatoria. Un bundle non firmato è accettato solo con insecure_skip_verify: true, per lo sviluppo. Con queste sorgenti le revisioni sono versionate fuori da manent (Git, CI, opa test): manent policy push e approve non sono disponibili, e policy list mostra la revisione in vigore.
Freeze a runtime#
Le finestre di freeze si possono anche creare e chiudere a runtime, con entrambi i motori, senza modificare manent.yml:
manent freeze add --name incidente-881 --reason "incidente 881" -e prod [-a 'shop-*'] [--region us-1] --for 4h [--hard]
manent freeze add --name chiusura-anno -e prod --from 2026-12-31T18:00:00+01:00 --until 2027-01-02T08:00:00+01:00
manent freeze list
manent freeze delete incidente-881
- Effetto. Un deploy reale nella finestra viene rifiutato subito, e uno già in coda fallisce all'avvio. Un rollout a ondate in corso si ferma prima dello stadio successivo e riprende quando la finestra si chiude.
- Hard. Con
--hardla policy non può esentare la finestra efreezeOverridenon la supera. - Permesso. Serve
freeze.writesu ogni pattern di applicazione e ambiente della finestra: un pattern*richiede il permesso su*. Lo concede il ruolo predefinitorelease-manager. - Elenco.
manent freezese la pagina Calendario le mostrano con il loro autore (dynamic,created_by). Si possono eliminare solo le finestre create a runtime.
Determinismo e registrazione#
Una decisione si prende una volta, in un punto registrato, e poi si riusa:
| Fase | Dove | Effetto | Registrata in |
|---|---|---|---|
submit |
accodamento (deploy, promozione, rollback, ogni deploy di una release) | divieti, freeze, requires, obblighi |
la richiesta: revisione, regola, hash dell'input (policy nello stato) |
start |
step config |
divieti e freeze arrivati dopo l'accodamento | output dello step (policy_revision) |
stage |
step policy@<stadio>#<n> prima di ogni stadio del rollout |
freeze per region (pausa) o divieto (il rollout si ferma) | output dello step |
tick |
step policy di ogni esecuzione di una schedule ricorrente |
gli obblighi di quell'esecuzione | la richiesta dell'esecuzione |
Una recovery rilegge le decisioni registrate e non rivaluta mai: una revisione cambiata nel frattempo non cambia la sequenza degli step. I workflow accodati prima dell'introduzione della decisione mantengono il comportamento precedente.
Gli obblighi si fissano all'accodamento: una revisione più severa non invalida un'approvazione già data. Con governance.start: strict l'avvio rifiuta anche un deploy per cui le approvazioni o il change management sono aumentati dopo l'accodamento: va accodato di nuovo.
Indisponibilità#
| Situazione | Effetto |
|---|---|
| Nessuna revisione caricabile all'avvio | /readyz risponde 503; i deploy reali sono rifiutati con 503 e lo step config viene ritentato. Dry-run, letture e annullamenti funzionano. |
| Sorgente irraggiungibile, revisione già caricata | resta in vigore quella caricata; oltre max_staleness come sopra. |
| Errore di valutazione (errore a runtime, timeout, decisione non valida o indefinita) | rifiuto, mai via libera: 422 all'accodamento, retry negli step. |
Permessi e audit#
| Azione | Consente |
|---|---|
policy.read |
Elencare e leggere le revisioni. |
policy.write |
Caricare revisioni, valutarle e misurarne l'impatto. |
policy.approve |
Approvare le bozze e tornare a revisioni precedenti. |
freeze.write |
Creare ed eliminare freeze a runtime. |
Le azioni policy.* non dipendono da applicazione e ambiente, come platform.read; tra i ruoli predefiniti le concede solo admin. Arrivano sempre da rbac.bindings di manent.yml, anche quando gli altri permessi li decidono le revisioni (Permessi come policy): una revisione non può concedersi il potere di cambiare la policy.
Ogni operazione è registrata nell'audit (policy.push, policy.activate, freeze.create, freeze.delete). Ogni decisione è nei log (e arriva al SIEM) con revisione e hash dell'input.
Break-glass. Se una revisione blocca tutto, o se durante un incidente manca un secondo approvatore, un operatore con accesso al database può agire fuori dai permessi. L'operazione resta registrata nell'audit.
manent-server admin policy-activate 12 --reason "INC-881: la revisione 13 blocca l'hotfix"
manent-server admin freeze-delete incidente-881 --reason "freeze chiuso dall'incident manager"
Metriche#
| Metrica | Descrizione |
|---|---|
manent_policy_revision_info{engine,source,revision} |
Revisione in vigore nel processo. |
manent_policy_age_seconds |
Secondi dall'ultima lettura riuscita di policy e freeze. |
manent_policy_load_errors_total |
Letture fallite (revisione rifiutata o sorgente irraggiungibile). |
manent_policy_evaluations_total{phase,outcome} |
Valutazioni per fase ed esito (allowed, denied, frozen, error, unavailable). |
manent_policy_eval_duration_seconds{phase} |
Durata delle valutazioni. |
Un allarme su manent_policy_age_seconds che si avvicina a max_staleness arriva prima dei rifiuti.
API#
| Metodo | Percorso | Descrizione |
|---|---|---|
GET |
/api/v1/policies |
Revisioni e policy in vigore. |
POST |
/api/v1/policies |
Carica una bozza ({"files": {...}, "comment": ...}). |
GET |
/api/v1/policies/{revision} |
Una revisione con i suoi file (current: quella in vigore). |
POST |
/api/v1/policies/{revision}/evaluate |
Decisione su un descrittore (phase, kind). |
POST |
/api/v1/policies/{revision}/access |
Permessi di un'identità, con rbac.engine: opa (simulazione). |
GET |
/api/v1/policies/{revision}/impact?since=30d |
Decisioni dei deploy reali ricalcolate. |
POST |
/api/v1/policies/{revision}/approve |
Approva una bozza. |
POST |
/api/v1/policies/{revision}/activate |
Torna a una revisione precedente. |
POST |
/api/v1/freezes |
Crea un freeze a runtime. |
DELETE |
/api/v1/freezes/{name} |
Elimina un freeze creato a runtime. |