Vizier Central
The same binary that runs one operator on one tree also runs a control plane for a whole team. Central adds the three things a local console structurally cannot have: roles that hold, one evidence floor per environment, and history that outlives the process. You host it, in your network, against your Postgres.
What it does not hold
No cloud credentials. Not encrypted, not envelope-encrypted, not “just the secret”. A cloud account here is a pointer to an identity you already control - a role ARN, a subscription and client id, a service account to impersonate - and the runner federates for short-lived credentials at run time. It holds no Terraform state either: state stays in your backend. A full compromise of this database teaches an attacker which role ARNs you use, and that is the whole blast radius.
Which way the arrows point
Central does not dispatch work. A local vizier - a laptop, a CI job, a self-hosted runner - announces a run it is about to perform, is told whether it may and what evidence this organisation demands, performs the run with its own credentials, and reports the outcome with the receipt. Report-in, not dispatch-out.
Start -> "about to apply env X at commit Y, citing change Z"
Central answers with the floor to enforce, or refuses.
<run> on your machine, your credentials. Central is not involved.
Finish -> outcome + receipt, kept verbatim.That direction is not a limitation to be lifted later. A control plane that dispatched would need a credential for every account it deploys into, which is the one thing this design refuses to become.
1. Run it
Postgres 14 or newer. The schema is applied on start, so an empty database is enough. It speaks plain HTTP and expects your own TLS termination in front of it - an ingress, a reverse proxy, a tunnel - and binds loopback by default so that putting it on a network is a decision somebody makes rather than one they inherit.
export VIZIER_DATABASE_URL='postgres://vizier:...@localhost:5432/vizier'
vizier central init --org "Your Company" --owner [email protected]
# prints one token, ONCE. Only its hash is stored.
vizier central serve --listen 127.0.0.1:84802. See it working, before you commit to anything
A fresh control plane is empty, and an empty estate map is not an argument for anything. One command fills it with a worked example so you can judge the thing rather than imagine it. It refuses an organisation that already has projects, so it cannot touch a real estate, and it is not part of the install: skip it and nothing is different.
vizier central demo # three projects, five environments, three accounts
vizier central demo --remove # deletes exactly what it created, by idThe estate it writes is deliberately unfinished. Two of the five environments have no cloud account bound, one holds a floor that warns rather than refuses, and one project has no repository recorded. Those are the states these screens exist to make visible, so a demonstration that tidied them away would be showing you a product nobody has. While the data is there the console says so on every screen, and the cloud accounts are pointers with nothing behind them: nothing in the example can reach a real account.
3. Add people
Run on the host: this command talks to the database directly, which is how the first tokens for a new deployment get made. Afterwards an owner can add members from the console.
vizier central token --email [email protected] --role operator
vizier central token --email [email protected] --role admin| Role | May |
|---|---|
| viewer | Read the estate, the runs and the receipts. Nothing else. |
| operator | Plan and apply. Raise changes for review. Cannot destroy. |
| admin | Destroy, configure projects, environments, accounts and trusted keys, review changes, revoke any member’s API token, read the audit log. |
| owner | Manage people, rename the organisation, and weaken an environment’s floor. |
The console has a Permissions screen that reads the server's own policy table rather than repeating it, so it cannot describe a system that no longer exists. Every action and the least role that may perform it is written out at docs/rbac, which is the version to send somebody before they have an account.
Tokens, and taking one back
An API token is the only credential Central has. It is shown once when it is minted and never again, because only its hash is stored - there is nothing to recover and nothing for an attacker who reaches the database to replay.
People leave, and laptops get lost. Revoking is immediate: every request re-reads the row, so there is no cached session and no self-describing bearer to wait out. The next request made with that token is refused.
# what exists. scope=mine is your own; scope=org needs tokens.manage.
$ curl -H "Authorization: Bearer $TOKEN" .../api/v1/tokens?scope=org
{"scope":"org","tokens":[
{"id":"3e514e72-...","userEmail":"[email protected]","label":"ci-runner",
"prefix":"vzc_a996ec","lastUsedAt":"..."}]}
$ curl -X DELETE -H "Authorization: Bearer $TOKEN" .../api/v1/tokens/3e514e72-...
200
# and immediately, on the revoked token
$ curl -H "Authorization: Bearer $REVOKED" .../api/v1/whoami
401A revoked token is kept, not deleted, and the list keeps showing it with the time it was revoked. A credential that never existed and one that was withdrawn after an incident are opposite facts, and the audit log names token ids; lastUsedAt is how you work out the exposure window afterwards. The console has the same screen, with revoked tokens drawn differently rather than hidden.
You can always revoke your own. Revoking somebody else's needs admin, not owner: revoking can only ever subtract permission, and a kill switch that waits for one particular person to wake up is not a kill switch. There is deliberately no API that MINTS a token - a stolen admin token must not be able to issue itself a replacement for the one you just took away. New tokens are made on the host with vizier central token.
4. Set the floor
An environment declares how much evidence it demands: a status floor, whether a signature is mandatory, whether sources must be pinned, and whether a failed check refuses the run or merely reports it. Production demanding more than a sandbox is the reason environments are rows at all.
The floor is handed to the runner at the start of every run, so nobody has to remember the right flags - and nobody can quietly not pass them. A run that asks for a weaker verify-mode than its environment demands is refused before it starts.
vizier: below this environment's floor
prod demands verify-mode=enforce and this run asked for off.
An owner can lower the floor; a runner cannot.5. Require review
Branch protection, for infrastructure. Each environment carries a count of how many people other than the author must approve a change before it may be applied there. Zero is no review, one is four eyes, two is what a regulated team usually wants on production. It defaults to zero: a control that arrives by surprise gets switched off in a hurry by whoever is mid-incident, so turning it on is a deliberate act - and turning it back down is treated as weakening the floor, which needs an owner.
An environment can also say which role those approvals must come from. Left unset, any reviewer counts - that is what every environment does by default. Set it to ownerand production needs an owner's sign-off specifically: an admin may still review, still comment, and still block by asking for changes, but their approval no longer satisfies the count. Higher rank does not substitute, because “an admin is nearly an owner” is exactly the reasoning the requirement exists to rule out.
Widening it is weakening the floor, and it is the quiet one. Going from “an owner must sign this off” to “anyone may” leaves the count at one and changes nothing visible, while removing the control entirely - so it needs the same owner permission as lowering the count. Narrowing needs no permission: fewer people being able to approve is not a weakening.
The role recorded against an approval is the one that person held when they approved. A later change of role does not unmake a sign-off they already gave, and does not retroactively satisfy one they did not.
A change is a pull request whose diff is the plan. A code diff tells you what the HCL says; it does not tell you that this apply replaces a database.
# plan first: the plan is what gets reviewed
vizier run-all plan --plan-dir .plans
# the engineer proposes, pinned to an exact commit, with the plan attached
vizier central propose \
--title "Add a read replica to the claims database" \
--head 8f2c41a9db7e5630c8a417fd2e9b05c3417ae62d \
--plan-dir .plans \
--verb apply
# somebody else reviews. It must be somebody else.
vizier central changes
vizier central review 58384dc6 --approve --note "Replica only, no data resources."
# and the apply is now allowed, at that commit and no other
vizier apply --dir envs/prod --change 58384dc6What the reviewer opens is a picture of that plan: one box per unit, coloured by what it does, with the dependency edges between them. It answers the question somebody actually has - what does this touch, and does any of it go away - before they read a line of HCL. A unit that creates three things and destroys one is drawn as destroying, because that is the fact that decides whether this needs a careful read.
A unit whose plan is missing or unreadable is drawn dashed and marked not planned, never as a unit that changes nothing: the two look identical on a screen and mean opposite things. If no plan is attached at all, the screen says so plainly rather than drawing an empty canvas - an approval given then is trust in the author, not a judgement about what will happen, and it should be obvious which one is being asked for.
Change ids resolve from a unique prefix, so the short id vizier central changesprints is what you paste. The commit defaults to this repository's HEAD; pass --ref to pin one deliberately.
What a governed run does
Once a tree is bound, plan, apply, destroy and run-all report into the control plane on their own. Nothing new to remember, and nothing to forget.
$ vizier apply --dir envs/prod
control plane: https://vizier.your-company.internal - prod - run c0c57866
floor: envs/prod: min_status -> live_tested
floor: envs/prod: require_signed -> trueThe floor is applied, not merely received, and it raises only: a unit that already demands more than the company minimum keeps its own setting. What it raised is printed, so nobody has to wonder why the run demanded more than their vizier.hcl says. Refusals happen before a single resource is planned, so they cost nothing:
vizier: this environment requires review: prod needs 1 approval(s) before an apply.
vizier: below this environment's floor: prod demands verify-mode=enforce, this run asked for off.
vizier: that is not the commit that was approved: approved 8f2c41a..., this run carries deadbeef...The outcome is reported either way, and especially on failure: a history containing only successes cannot answer the question people actually bring to it. The receipt stored centrally is byte-for-byte the one --receipt would have written, so the shared record cannot disagree with the file on somebody's disk.
The two rules that make review mean something
Both are enforced in the database, in the same statement that writes the row, rather than in a handler or a client. A rule enforced in a client is a rule anybody can edit out.
- The author cannot approve their own change. Not a role check - an admin who raised a change still cannot pass it. Without this, four-eyes is true on the org chart and false in the room.
- An approval is pinned to one commit. A review records the commit it was given, and an apply must carry the commit that was approved. Approve one, apply another, and the approval was theatre. This is also why a change is immutable once raised: a new commit is a new change, not an edit to this one.
Three more follow from those: an approval is spent once, so a retried pipeline cannot apply twice on one decision; a review given on an earlier commit is shown but never counted; and a run that reports itself as passed while also reporting refusals is rejected rather than stored.
6. Certify it, if a client wants proof
Review is your team agreeing. Certification is a service you do not run executing the commit in a real sandbox and returning a receipt anybody can check. When a client wants to verify your pipeline rather than take your word, that is the artifact.
$ vizier certify change 58384dc6
certifying Add a read replica at c4d8e1f0a72b
run r-8812 (--attach r-8812 to resume watching)
running
succeeded
recorded check "certify" = passed on Add a read replicaIt reads the change from Central, submits the commit that change pins - never whatever is checked out locally - waits, and records the verdict as a check. A red verdict blocks production, and nothing new enforces that: a change carrying a check that is not green was already unmergeable.
The command holds two credentials, because these are two services: your Central token annotates the change, the attestation token pays for the sandbox run. It refuses locally, before anything is billed, if the change is already closed, names no source, or pins something that is not a full commit id. A run that applied and did not tear down is recorded as failed, not passed: a leak is an incident, not a result.
A tree that does both names both, because Central reviews and the attestation service executes:
{
"schemaVersion": 1,
"kind": "central",
"endpoint": "https://vizier.your-company.internal",
"attest": "https://iac-bazaar.com",
"environment": "<environment id, from the console>",
"onUnreachable": "fail"
}7. Point a tree at it
A tree names its control plane in vizier.control.json at the root. The file is meant to be committed - that is how a team shares where the control plane is - and it refuses to hold a token, because a credential in a committed file is a credential in your git history. Your identity is stored separately, per machine.
{
"schemaVersion": 1,
"kind": "central",
"endpoint": "https://vizier.your-company.internal",
"environment": "<environment id, from the console>",
"onUnreachable": "fail"
}vizier login --endpoint https://vizier.your-company.internal
vizier central statusonUnreachable defaults to fail, and that is the safe direction: an attestation that did not happen is not an attestation that passed. Set it to warn only as a deliberate choice, and it is recorded in the receipt rather than inferred from a gap.
State locks
A lease is not a second state lock. Your backend already locks state, and it does that job well. A lease exists because it is issued by something the person running the apply does not control, and only against an admission receipt showing a policy was in force and nothing was refused. That is what turns verification from advice into a control: an unverified run is not told to wait for the lock, it is told it may not have one.
Leases are held in Central's Postgres and scoped to your organisation, so they survive a restart and one company can neither block nor break another's. Each grant carries a fencing number that only ever increases, including across a release: a runner that stalled past its lease cannot come back and act as though it still held one, because whatever it writes carries a number the next holder has already beaten.
vizier admit --lease https://central.example.internalThe console's State locks screen answers the question somebody actually opens it with - who holds this, since when, and until when - and separates what is held from what has lapsed and not yet been taken over. Breaking one is an admin act, because it overrules a process that may still be writing, and it is recorded in the audit log naming who lost it. The lock key is a hash of the backend address, never the address: that names a bucket and a path in your account, and the control plane has no business knowing either.
Precisely what the command above does today, because the difference matters: vizier admit takes the lease and gives it back in the same command. That is right for what it is. A check that held the key until it exited would block the very apply it just cleared, and the value of asking Central for one is the verdict, which is the half a person running the apply does not control. A serialised apply is a different command: holding the key from plan through apply is a longer lifetime than a check has, so it belongs in the step that applies, and vizier apply --lease holds it there for the whole run. So the State locks screen shows a moment for a check and a whole run for an apply. What Central provides is the durable, organisation-scoped, fenced lock service the applying step will hold against.
The consoles
Central serves its own console at the same address: an estate map you can drag to bind accounts to environments, the floor ladder ordered by strength rather than by name, the review queue, run history with every receipt kept verbatim, the state locks currently held, members, permissions, the audit log, and a live team activity feed.
The local console (vizier ui) gains a Control plane screen showing what this tree is bound to, who this machine acts as, the floor it will be held to, and what is waiting for review. Approving happens in the control plane console rather than there: the local console is one operator on one tree, and a review is a decision about somebody else's work.
Operating it
- Back up Postgres. It holds your estate, your people and every receipt. It holds nothing that would let an attacker into a cloud account.
- Terminate TLS in front of it. It speaks plain HTTP and says so loudly if you bind it to anything but loopback.
- One deployment serves one company.
initrefuses to run twice. - The live feed is in memory. Restart and every console reconnects and resumes from live, having missed what happened in between. The durable record is the run list and the audit log. Do not run two replicas behind a load balancer and expect each to see the whole organisation.
Vizier is a free download at github.com/CyberCoreSystems/vizier. Central is a separate, paid build: the free download does not contain it, and its central command tells you so rather than pretending the feature is missing.
Once you subscribe, the Central build and your licence are both on your account page, with the sha256 of each archive beside it. It is not on the public releases page, so those links check your account and only work while you are signed in.