One CLI that turns a fresh VPS into a hardened PostgreSQL 18 server, and then stays the only way in.
From your laptop (or from a coding agent), vaire takes a new Debian 13 or Ubuntu 26.04 server, locks it down, installs PostgreSQL 18 with PgBouncer and nginx, issues its TLS certificate, and connects you to it over WireGuard. Each database you create gets its own tuned PostgreSQL cluster with a size and resource limits, the way a managed provider gives each database its own instance. Every later operation goes through the same binary, over SSH, inside WireGuard. There is no server control daemon, web UI or open control API port; an operator-side shared tunnel daemon keeps concurrent CLI commands on one WireGuard connection per context.
Status (2026-10-19): preparing the first v1 alpha; not yet tagged or published. Phases 0β5 have live qualification evidence, including backups, disaster recovery, protected writes, observability, local bootstrap and automatic SSH-lockdown revert. Known coverage limits are recorded in manual testing. Followers, extensions and multi-node remain future work. See the roadmap and draft alpha notes. Alpha formats are not frozen; a later alpha may require a rebuild and restore.
| Area | Commands |
|---|---|
| Setup | vaire server init (remote over SSH, or--local on the server),--resume |
| Access | vaire login ,vaire context list|show|use|remove|set-address ,vaire ssh ,vaire tunnel |
| Desired state | vaire plan ,vaire apply ,vaire config show|get|set|edit|validate |
| Databases | vaire db list|create|resize|drop|credentials|fork|protect|unprotect |
| Protection | vaire protect init|status (two-factor code),vaire db protect|unprotect |
| SQL | vaire sql (multiple statements and ordered results, JSON-friendly),vaire psql ,vaire console (usql) |
| Tuning | vaire tune show|explain|apply |
| Backups | vaire backup setup|now|list|verify ,vaire dump ,vaire dump list|get |
| Restores | vaire restore <db> (--time ,--latest ,--set ,--dump ),vaire restore status|promote|undo|abort|cleanup|drill |
| Disaster recovery | vaire server backup now|list|verify|export (the master bundle),vaire server init --restore ,vaire db recover <db>|--all |
| Jobs | vaire job list|follow |
| Health | vaire status [--watch] ,vaire doctor [--only security] ,vaire top ,vaire queries |
| Evidence | vaire activity list|show|health ,vaire diagnostics collect|inspect (local metadata-only history and portable incident files) |
| Metrics | vaire metrics show|rotate : an authenticatedhttps://db1.<domain>/metrics/... for Prometheus or Grafana Alloy, with atargets.json for service discovery |
| Maintenance | vaire maint vacuum|analyze|reindex|report|kill (repack waits for Phase 7), nightly and weekly maintenance timers |
| Operations | vaire logs ,vaire secrets export ,vaire upgrade |
| Agents | vaire commands --json ,vaire guide agents ,vaire skill install |
Commands from later phases are already in the tree and return not implemented yet.
New in this checkout, awaiting live qualification: vaire activity list|show|health and vaire diagnostics collect|inspect provide local, metadata-only operation history and portable incident evidence. No logging database, external collector or raw-log export is required. See activity and diagnostics.
curl -fsSL https://vaire.mbvlabs.com/edge/install.sh | bash
vaire server init --host root@203.0.113.10
vaire db create app --size small # a cluster of its own; prints the URLs and passwords once
vaire sql --db app "select now()"
vaire console app # interactive usql session
vaire console app --write # run DDL as the database owner
On another machine, vaire login and paste the transfer bundle from the handoff.
laptop / CI / agent VPS db1.<domain> (the master node)
βββββββββββββββββββββββββββββββββ βββββββββββββββββββββββββββββββββββββββββββ
β vaire (operator mode) β β wg0 10.42.0.1 β
β β context + credentials β WireGuard UDP 51820β β sshd (port 22 reachable on wg0 only) β
β β userspace WireGuard βββββββͺββββββββββββββββββββββΆβ β
β β (no root, no wg0 needed) β β vaire (node mode), run per command: β
β β in-process SSH ββββββββββββΌββββ inside WG βββββββΆβ β versioned steps + embedded scripts β
β β pgx / usql / psql ββββββββββΌββββ inside WG βββββββΆβ β /etc/vaire/cluster.toml (desired state)β
βββββββββββββββββββββββββββββββββ (5432 / 6432) β β journal of applied steps β
β β systemd timers β vaire node run β¦ β
β β
β nginx Β· 2 PgBouncers Β· one PostgreSQL β
β 18 cluster per database β
βββββββββββββββββββββββββββββββββββββββββββ
- One binary, two roles. On your machine
vaireis the operator CLI. On the server the same binary runs asvaire node β¦, called over SSH for each command and by systemd timers. Operator and node versions are kept in lockstep: a mismatch is refused unless you pass--auto-upgrade-nodes, which pushes the matching signed build. - Desired state in, convergence out. The server and its databases are described by one TOML file on the server.
vaire planshows what would change andvaire applyconverges, and it's always safe to run again. Steps have versioned IDs (tls-v1, andpostgres-config-v1@appfor one database's cluster), and a journal records what ran, so an upgrade only re-runs the steps that changed. - vaire owns the config it writes. Managed files are rendered from templates and checked for drift.
ALTER SYSTEMis disabled so nothing changes behind vaire's back.
Every database is its own PostgreSQL cluster: vaire db create app makes the cluster 18/app with its own data directory, socket, WAL stream, configuration, restarts, pgBackRest stanza and systemd resource limits. You only ever name the database; the cluster sits underneath. Things that belong together share one database with several schemas.
db1.<domain>
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β :443 nginx (ALPN postgresql) ββ β
β :6432 transaction pooler ββββββ routes by database name β
β :5432 session pooler routes by database name β
β β β
β ββ app β postgresql@18-app large socket .s.PGSQL.5433
β ββ billing β postgresql@18-billing medium socket .s.PGSQL.5434
β ββ staging β postgresql@18-staging small socket .s.PGSQL.5435
β β
β clusters listen on Unix sockets only; nothing but the poolers faces β
β the network β
ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- Sizes. A database has a size, like a provider's plan:
nano(256 MiB, 20 connections),small(512 MiB, 30),medium(1 GiB, 50, the default),large(2 GiB, 100),xlarge(4 GiB, 200). The size drives its tuning, its CPU weight and its connection limit.--memoryoverrides the memory, andvaire db resize app --size largechanges it later: limits and reload-only settings change live, and a restart, when one is needed, restarts only that database. - A memory budget. The server keeps a reserve (
max(512 MiB, 25% of RAM)) for the OS, the poolers, nginx and vaire; the databases share the rest.vaire tune showprints the sheet: each database's size, limits and current use, and what's allocated or free. - No overcommit by accident.
db create,db forkand a growingdb resizeare refused when the new size would take the sizes past the budget (memory_overcommitted); the error gives the numbers and the largest size that still fits, as a command to run, for examplevaire db create big2 --size small. Nothing steps down by itself.--allow-overcommitgoes ahead with a warning; under load the databases are then throttled rather than killed. A database PostgreSQL couldn't start at all is always refused (would_not_start). - Rough density. An idle cluster's own processes cost about 19 MiB (measured on PostgreSQL 18 with a
smalldatabase: the cgroup'sanonpluskernelmemory).shared_buffersis charged as it fills, not up front, and the page cache is reclaimable. A 2 GiB VPS takes 2smalland 1nano; a 4 GiB VPS takes 1mediumand 3small; an 8 GiB VPS takes 1large, 2mediumand 2small.
Every server (node) has one hostname, <node>.<domain>; the first one is db1.<domain>. The database name in the URL's path picks the database, which is how PostgreSQL URLs already work, so DNS needs one record per server, not per database:
postgres://app_app:β¦@db1.example.com:6432/app?sslmode=verify-full&sslrootcert=system app, transaction pooler
postgres://app_app:β¦@db1.example.com:443/app?β¦&sslnegotiation=direct app, direct TLS on 443 (PG17+ clients)
postgres://app_migrate:β¦@db1.example.com:5432/app?sslmode=verify-full&sslrootcert=system migrations and tools, session pooler
Creating, forking, restoring, resizing or dropping a database never touches DNS or the certificate. Multi-node arrives in Phase 10, and moving a database to another server will change its URL's host.
- Isolation is cgroups, not a VM. Each cluster's
MemoryHigh(its memory plus 25%), CPU weight and I/O weight are set by systemd; a hardmemory_maxorcpu_quotais opt-in.MemoryHighthrottles, it doesn't partition. A managed provider's VM is a harder boundary. - I/O weights depend on the disk scheduler. They only work with BFQ or the kernel's
io.costmodel; on a typical virtio disk they do nothing, andvaire tune showsays "not enforced". - Disk space and the page cache are shared. A database that fills the disk affects its neighbours.
- Density costs. Each cluster carries its own processes (about 19 MiB idle) and buffers; a 1 GiB VPS isn't a sensible home for several databases.
- One server is a single point of failure until HA (Phase 11).
- Recovering a lost server. The server backs its own state up after every change: an age-encrypted master bundle in the bucket under
<prefix>/master/(vaire server backup list). When the server is lost, power the old VPS off or destroy it, create a new one, and runvaire server init --restore --host root@<new-ip>from your machine: the new server takes over the old one's WireGuard key, SSH host keys and certificate, so your context keeps working; thenvaire db recover --allbrings each database back from its stanza. Move thedb1.<domain>record to the new IP, or use a provider IP that moves between servers (a Hetzner Primary IP, a floating IP) so nothing changes in DNS; a short TTL on that record shortens the wait for clients. Without a bucket, keep copies withvaire server backup export; the server comes back from one with--bundle-file, but the databases' data doesn't: configure a bucket for production.
After bootstrap the server has exactly these public ports open, enforced by ufw (default deny incoming):
Internet
β
βββββββββββ¬ββββββββββΌβββββββββββ¬ββββββββββββββββββββββββββ
β β β β β
51820/udp 80/tcp 443/tcp 6432/tcp, 5432/tcp 22/tcp
WireGuard ACME nginx only from client_allowlist CLOSED
β HTTP-01 stream (expose_pgbouncer / (wg0 only)
β β β expose_postgres)
βΌ βΌ βΌ βΌ
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β wg0 10.42.0.1 ββ sshd :22, transaction pooler :6432, β
β session pooler :5432 β
β (WireGuard peers always reach these) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
- SSH only over WireGuard. Port 22 is allowed only
in on wg0. Root login and password authentication are off, and only the admin user may log in, with the operator's key. - Commit-confirm lockdown. Before vaire closes public SSH, it snapshots sshd and firewall state and arms a server-side timer. If vaire doesn't confirm a working SSH-over-WireGuard session within two minutes, the server reverts on its own. A broken tunnel can't lock you out.
- Break-glass is the provider console plus
vaire node unlock-ssh, which re-opens public SSH with a password for the admin user for a fixed window. The steps are in the secrets handoff. - Database ports are closed to the public by default. WireGuard peers can always reach 6432 and 5432.
client_allowlistwithexpose_pgbouncer/expose_postgresopens them to specific CIDRs. Both are PgBouncers; no PostgreSQL cluster has a TCP listener. - fail2ban watches sshd. unattended-upgrades installs security updates only, with PostgreSQL, PgBouncer and nginx held for controlled updates.
client (sslmode=verify-full) VPS
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
ββ :6432 ββββββββββββββββββββββΆβ transaction pooler (TLS) ββ β
ββ :5432 ββββββββββββββββββββββΆβ session pooler (TLS) βββββΌβ unix socket ββΆ the β
β β database's cluster β
ββ :443 ββββββββββββββββββββββΆβ nginx stream, ssl_preread β (by database name) β
(sslnegotiation=direct, β ALPN "postgresql" + allowed source β
PG17+ clients) β β terminate TLS β transaction pooler β
β same ALPN, other source β handshake rejected β
β SSLRequest (no direct) β "no TLS" (N) β
β anything else β HTTPS: /metrics, else 404 β
β β
ββ :80 ββββββββββββββββββββββΆβ nginx: /.well-known/acme-challenge/ only β
ββββββββββββββββββββββββββββββββββββββββββββββββββββ
-
443 needs
sslnegotiation=direct. nginx routes 443 by peeking at the TLS ClientHello, so it can't answer the PostgreSQL SSLRequest that clients send by default. Withoutsslnegotiation=direct, nginx answers "no TLS here" and closes: libpq (withsslmode=requireor stricter) saysserver does not support SSL, but SSL was required, pgx saysserver refused TLS connection. Addsslnegotiation=direct(PostgreSQL 17+ libpq, pgx 5.7+), or use port 6432, which speaks the normal negotiation. A direct-TLS client from an address that isn't allowed gets its handshake rejected: libpq saysSSL error: tlsv1 unrecognized name, pgxremote error: tls: unrecognized name. -
vaire is its own ACME client. An hourly
vaire-certstimer issues and renews the certificate for the node's hostname (db1.<domain>) over HTTP-01, installs it for both PgBouncers, nginx and every cluster, and reloads them. Until the first certificate arrives, a placeholder certificate lets everything start. -
The DNS record must point straight at the server (DNS only, not proxied).
-
A
self-signedmode exists for servers without a domain. Connections are encrypted but not verified, and vaire warns about that. -
Every cluster listens on its Unix socket only (
listen_addresses = ''), and itspg_hba.confallows nothing else. The poolers terminate TLS and require it on TCP, withscram-sha-256. Thepostgressuperuser is local-socket only. -
allow_alter_system = off,REVOKEofCONNECTand ofpublicschema rights fromPUBLIC,idle_in_transaction_session_timeout, and connection and lock logging. -
Settings are tuned per database from its size and workload profile (
vaire tune explain --db appshows why each value was picked).
<db>_owner NOLOGIN, owns the database and every object in it
β²
β member, SET role = <db>_owner on login
<db>_migrate DDL: migrations β :5432 (session pooler; the transaction pooler refuses it)
<db>_app SELECT/INSERT/UPDATE/DELETE β :6432 or :443 (transaction pooler), statement_timeout
<db>_ro SELECT, read-only by default β :6432 or :443 (transaction pooler), reporting and read-only clients
in every cluster:
vaire_admin CREATEDB/CREATEROLE, not superuser (operator)
pgbouncer_auth may only call the auth_query functions
vaire_monitor pg_monitor
Run migrations as migrate and the application as app. The transaction pooler refuses the migrate role: in transaction mode, advisory locks, session SET and temporary tables can land on a different connection, so migrations go through the session pooler on 5432, where a client keeps its server connection for the whole session. From outside WireGuard, use vaire tunnel --db app --target postgres or set expose_postgres = true. A compromised app can change data but can't drop or change the schema. Default privileges give app and ro access to every new table without extra grants. The service passwords (vaire_admin, vaire_monitor, pgbouncer_auth) are the same in every cluster, so adding a database never makes your secrets handoff stale.
- vaire generates server and database passwords. The optional master password for the secrets-export gate is chosen by the operator.
- The server keeps only SCRAM verifiers, plus the two service passwords PgBouncer and the exporters need at runtime.
- At the end of
server init, vaire prints thesecrets handoff once:- every secret in plain text, grouped for a password manager;
- an ASCII-armored
agetransfer bundle (scrypt), with a generated passphrase.
- You type
storedto confirm you've saved it, and vaire offers to clear the screen. Nothing is written to disk unless you ask with--secrets-out. vaire loginon any machine takes the bundle and passphrase and sets up acontext : credentials under~/.config/vaire/contexts/<server>/, files 0600.- Later full-document reveals (
vaire secrets exportand replacement handoffs) require a master password or a node-verified authenticator code at a real terminal. With neither configured, set one usingvaire secrets password setorvaire protect init. This is a command gate, not encryption of context files at rest. - Releases are signed with cosign.
vaire upgradeand node upgrades verify the signature and the sha256 before swapping binaries.
A protected database refuses db drop, a shrinking resize, credential rotation and restore promote/undo, even with --confirm. Lifting the protection asks for a current 6-digit code from an authenticator app, typed at a terminal and checked on the node, so an agent with a shell can't lift it by typing a word.
Writing to it takes a code too. vaire sql --write, vaire psql --write, vaire console --write and vaire maint kill ask for one when they start, before anything connects; a psql or console session then stays writable until you close it, and vaire sql --write asks on every run. Reads never ask. Without a terminal, with --yes or with --output json/ agent the write is refused, so an agent gets read-only access. The node enforces it: on a protected database the vaire_admin login in every context can only read, and a write runs as a two-minute login role the node creates after it checks the code. Vacuum, analyze and reindex, restores into a copy and forks work as before.
Set it up once with vaire protect init: the node makes the secret and keeps it root-only, and vaire shows it as a QR code. Once the code is confirmed, it asks which of your databases to protect (all of them picked to start with) and protects them in the same run; --all or --db app (repeatable) choose without asking. After that:
vaire db protect app billing # or --all; in effect at once, no apply
vaire db create scratch --protected
vaire db unprotect app # asks for a code from the authenticator app
vaire sql --db app --write "β¦" # on a protected database, asks for a code too
vaire protect status # every database, protected yes or no
Protecting a database is refused until vaire protect init has run. A code works once, and 5 wrong codes lock codes out for 15 minutes. vaire config set clusters.app.protected true still works too. Root on the server can still edit the files directly; this guards against agents and slips, not against someone who owns the box.
vaire console app
β
ββ usql ββplaintextβββΆ 127.0.0.1:<port> (loopback only)
β β
β vaire: SSLRequest + TLS handshake, verify-full
β against the node's certificate (db1.<domain>)
β β
βββββββββββββ userspace WireGuard βββΆ 10.42.0.1:5432 session pooler β app
vaire sql, vaire psql and vaire console all connect over the tunnel to the session pooler on 5432 of the node that holds the database, which routes by --db, and verify the certificate ( verify-full). They log in as vaire_admin, never as an application role, and are read-only unless --write is passed; with --write they act as <db>_owner, so new objects get the same owner and default grants as a migration. vaire tunnel --db app --target postgres forwards a local port to that pooler, which is how a migration tool on your machine reaches the database as <db>_migrate without opening 5432 to the internet. vaire console uses usql from PATH, or downloads a pinned release and checks its SHA-256. Arguments after -- go to psql as they are (vaire psql --db app -- -c "select 1"); a -c before -- is vaire's --context.
VPS S3-compatible bucket (R2, S3, B2, β¦)
ββββββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββ
β app, billing, β¦ (one cluster each) β β <prefix>/pgbackrest/ β
β ββWAL, continuously (β€60 s)βββββββββββββΌβββββββββββΆβ one stanza per database: β
β ββ vaire-backup-*@<db> timers ββββββββββββΌβββββββββββΆβ full Β· diff Β· incr sets, WAL β
β Sun full, MonβSat diff, staggered β β (aes-256-cbc, zstd) β
β Wed verify, monthly drill β β β
β ββ scheduled per-database dumps ββββββββΌβββββββββββΆβ <prefix>/dumps/<db>/ (age-encrypted)β
ββββββββββββββββββββββββββββββββββββββββββββ ββββββββββββββββββββββββββββββββββββββββ
- A stanza per database. Each database's cluster has its own WAL stream, so backups, point-in-time recovery, retention and verification are all per database, in one repository with one cipher. Everything names a database:
backup list --db,backup now --db,restore <db>,db fork <db>. - Restore window. A database can be rewound to any second between just after its oldest backup and its last archived WAL, which is at most about a minute old. The backup sets only make restores faster; the WAL fills the gaps between them.
retention_fullis the window in weeks (default 4, and a database can have its own withclusters.<db>.backup.retention_full). Retention is time-based, so extra full backups never shorten it. vaire backup setupprobes the bucket from your machine, generates the repository cipher (the age key for dumps already exists fromserver init), switches every database's WAL archiving on with a reload (no restart), and takes a first full backup of each. The bucket keys, cipher and age identity form therecovery kit , which goes into the secrets handoff: runvaire secrets exportafterwards. Backups are optional duringserver init; until a bucket exists,server initand everyapplywarnno_offsite_backup.- Staggered. Each database's timers run at their own offset inside the maintenance window, and only one full, diff, verify, drill or dump runs at a time on a server.
- Dropped databases stay restorable for their window.
db dropkeeps the stanza;vaire backup listshows it until a daily timer deletes it once the window has passed. - Restoring one database doesn't touch the others. A restore builds a new cluster from the database's stanza beside the live one, you check it, and only
promoteswaps it in. It needs free space for one copy of the database. vaire db fork <db> [--name <new>] [--at <time>]makes a separate database: a physical copy of the source (from its stanza at a point in time, or from the live cluster), renamed, with its own roles and new passwords, the source's size and settings (90-overrides.confincluded), its own stanza and a first full backup. It works without backups when there's no--at.- Long-retention dumps. A database that must be kept longer than the window gets scheduled
pg_dumparchives (clusters.<db>.backup.dump_scheduleanddump_retention), encrypted with age and streamed to the bucket.vaire dump getdecrypts one,vaire restore <db> --dump <id>loads it, andvaire db droptakes a final dump first. - Long work runs as detachable jobs. Backups, verify, restores, drills and forks run as systemd units on the node. Ctrl-C detaches, and
vaire job list/vaire job follow <id>re-attach. - Checked, not assumed.
vaire backup verifychecks every file of a database's stanza against its checksum, weekly by default. The monthlyvaire restore drillrestores each database into a throwaway cluster, runspg_amcheckon it and compares it with the live one; each database's drill falls on its own day of the month. Both results show invaire backup list.
The example rewinds app to just before a bad DELETE at 14:03 UTC. Every confirm token is <db>/restore, except cleanup's, which is <db>/cleanup.
Pick the target. All times are UTC.vaire backup list --db app shows the restore window: any second between its start and the last archived WAL can be restored. Use--time for a point in time (RFC 3339, or relative like-15m ),--latest for the newest WAL,--set <label> for one backup set, or--dump <id> (fromvaire dump list ) for a point older than the window.
vaire backup list --db app
Restore into a copy. This runs as a job on the node. Ctrl-C only detaches, andvaire job follow <id> re-attaches. Add--dry-run first to see the plan. The liveapp keeps serving throughout.
vaire restore app --time 2026-10-12T14:02:00Z --confirm app/restore
pgBackRest restores app 's stanza into a new cluster,app__restoring , which replays WAL up to the target and s there, read-only. Only one restore per database can be open at a time; if a previous one failed, the new one clears it.
3.
Check the copy before you promote.restore status compares row estimates with the live database. Query the copy directly through the session pooler to confirm it holds what you expect:
vaire restore status app
vaire sql --db app__restoring "select count(*) from users"
If it's the wrong point in time, vaire restore abort app dropsapp__restoring (the live database was never touched), and you can restore again.
4.
Promote. The transaction pooler sapp , the live cluster stops, the data directories swap places, andapp starts on the restored data with its own configuration and limits, finishes replaying to the target and opens on a new timeline. Clients see a few seconds of downtime plus that replay; other databases aren't touched. The URL, roles and passwords don't change, so the application reconnects without any change. A full backup starts right after. The old data is kept as the stopped clusterapp__pre_restore_<timestamp> .
vaire restore promote app --confirm app/restore
vaire sql --db app "select count(*) from users"
Undo or clean up. If the promoted data is wrong,undo swaps the old data back the same way, as long as the leftover still exists, and vaire records which timeline to follow so later restores don't wander onto the abandoned one. Once you're happy,cleanup drops the leftover and frees its space. Runvaire restore cleanup without a database to list leftovers.vaire db drop drops a database's leftovers with it.
vaire restore undo app --confirm app/restore # go back to what was live before the promote
vaire restore cleanup app --confirm app/cleanup # or: keep the restore, drop the old copy
vaire restore app --time β¦ --promote --confirm app/restore does steps 2 and 4 in one go, skipping the check. Use it only when you're sure of the target. When only some rows are needed, vaire db fork app --name app_restored --at <time> gives you a separate database at that point instead, without touching the live one.
A restore from a dump (--dump <id>) builds the copy from the dump instead, with the database's roles and passwords. On promote the database gets a new stanza, because its data has a new system identity; the old stanza stays in backup list as dropped for the rest of its window.
Installed from the distribution's apt repositories unless noted. Steps install only what is missing, and re-runs change nothing.
| Package | Installed by | Why |
|---|---|---|
curl ,ca-certificates ,gnupg ,lsb-release ,jq ,sudo |
base packages | Basics for the steps and the PGDG repository |
apt-transport-https |
base packages | Only on releases that still ship it |
ufw |
base packages | Firewall: deny by default, the allow rules above |
fail2ban ,python3-systemd |
base packages | Bans repeated SSH auth failures (reads the journal) |
wireguard-tools |
base packages | The wg0 interface the operator connects through |
chrony |
base packages | Time sync (TLS and logs depend on it) |
logrotate |
base packages | Rotates /var/log/vaire |
unattended-upgrades |
base packages | Security updates only; PostgreSQL, PgBouncer and nginx are excluded |
postgresql-common |
PostgreSQL packages | Adds the PGDG apt repository ( apt.postgresql.org ) andpg_createcluster , which makes each database's cluster |
postgresql-18 ,postgresql-client-18 ,postgresql-contrib |
PostgreSQL packages (PGDG) | PostgreSQL 18; the major version is held with apt-mark hold |
pgbouncer |
PostgreSQL packages (PGDG) | Two poolers: transaction mode on 6432 (Debian's pgbouncer.service ) and session mode on 5432 (vaire-pgbouncer-session.service ) |
nginx ,libnginx-mod-stream ,ssl-cert |
TLS step | ACME HTTP-01 on 80 and ALPN routing on 443; held with apt-mark hold |
pgbackrest (β₯ 2.59.3) |
PostgreSQL packages (PGDG) | Database backups, WAL archiving and restores; held with apt-mark hold . 2.59.3 is the floor because earlier releases have an encryption bug |
vaire |
server init /--auto-upgrade-nodes |
The node binary at /usr/local/bin/vaire , pushed from the operator and signature-checked |
| node_exporter 1.12.1, postgres_exporter 0.20.1, pgbouncer_exporter 0.12.1 | metrics step (upstream release binaries, sha256 pinned in vaire) | Metrics on loopback, served through nginx's authenticated /metrics; the same versions on Debian and Ubuntu |
No ACME client (certbot and the like) is installed: vaire issues certificates itself. vaire's bloat report embeds the estimate queries of ioguix/pgsql-bloat-estimation (BSD-2-Clause; vaire version --licenses prints the notice).
| Tool | Needed for | How you get it |
|---|---|---|
vaire |
Everything | install.sh , cosign-verified;vaire upgrade afterwards |
| WireGuard, SSH | Every remote command | Built into vaire (userspace WireGuard, in-process SSH). Nothing to install and no root needed |
psql β₯ 16 |
vaire psql only |
Your package manager; vaire offers the install command |
usql |
vaire console only |
On PATH, or vaire downloads a pinned release (v0.21.4) into ~/.cache/vaire/tools and checks its SHA-256 |
wireguard-tools |
vaire wg export --install only (not implemented yet) |
Your package manager; for a kernel WireGuard interface instead of the built-in one |
| Library | Used for |
|---|---|
spf13/cobra |
Command tree and flags |
Charm huh ,bubbles ,lipgloss |
Interactive prompts and progress output |
golang.org/x/crypto/ssh |
The SSH client |
golang.zx2c4.com/wireguard (wireguard-go + netstack) |
The userspace WireGuard tunnel |
golang.org/x/crypto/acme |
Issuing and renewing certificates on the node |
jackc/pgx |
SQL over the tunnel ( vaire sql ,db ,tune ) |
filippo.io/age |
The encrypted transfer bundle and long-retention dumps |
minio/minio-go |
Bucket probe on backup setup , and streaming dumps to and from the bucket |
sigstore/sigstore-go |
Verifying signed releases on upgrade |
pelletier/go-toml |
cluster.toml |
itchyny/gojq |
--jq output filtering |
rsc.io/qr |
The QR code vaire protect init shows for the authenticator app |
Operational commands support non-interactive structured JSON (--output json), with documented exit codes and --dry-run where it makes sense. Human-only secrets reveals and protected writes deliberately refuse unattended use. Destructive operations need a --confirm <token> that --yes never satisfies. vaire commands --json describes the whole surface, and vaire guide agents / vaire skill install load the operating guide into a coding agent.
just build # bin/vaire
go test ./... # unit and regression tests; no local server harness
just lint # golangci-lint, metadata and step checks, shellcheck, actionlint
- docs/MASTER.md : goal, decisions, architecture, roadmap and current status.
- docs/IMPLEMENTATION.md : the detailed spec for each phase.
- docs/RFC-cluster-per-database.md : why each database is its own cluster, and how that shapes every phase.
- docs/MANUAL-TESTING.md : live evidence, historical failures and remaining coverage limits.
- docs/README.md : document index, including release preparation and archived work records.
vaire is released under the MIT License. Embedded third-party code keeps its own licence (see above).