Versola needs about thirty secrets: encryption and MAC keys, signing key pairs, the Postgres password and the admin bootstrap password. This page explains where they live, what versola configure does with each one, and which ones you can replace safely. Read it before you restore a server, move to a new OpenBao, or rotate a value.
Where secrets live
localandvpstargets. In OpenBao (a Vault-compatible secret store, KV v2, mounted atsecret), atsecret/versola/<target>/<path>, where<path>is one ofauth,central,edgeandutils.versola configurestarts OpenBao, reads these paths, decides what is missing, writes any new values, and then resolves every${VAR}placeholder of the generated configuration into<service>.secrets.envfiles (mode600) in the deployment bundle. The services never talk to OpenBao at runtime; OpenBao has to be up only while you runconfigure.- Kubernetes. In Kubernetes Secrets that you create; the chart neither generates nor requires a particular secret backend. See Kubernetes.
Back up OpenBao’s data and the root token and unseal key in ~/.versola/openbao/vps-admin.json. A secret that protects stored data cannot be regenerated: if it is lost, the data it protects is unreadable.
| OpenBao path | Holds |
|---|---|
secret/versola/<target>/auth | Secrets loaded into the auth container. |
secret/versola/<target>/central | Secrets loaded into central, including the public halves of key pairs. |
secret/versola/<target>/edge | Secrets loaded into edge. |
secret/versola/<target>/utils | The private key of the utils client, apart from the services on purpose: central must never hold the key that authenticates as utils. |
The secrets
These are the secrets the vps schema lists. “On missing” is the policy explained below.
| Secret | Service | Protects | On missing |
|---|---|---|---|
ACCESS_TOKENS_SECRET | auth | access token key | generate |
AUTH_CODES_SECRET | auth | authorization codes | generate |
SESSIONS_SECRET | auth | SSO sessions | generate |
CONVERSATION_COOKIE_SECRET, SESSION_COOKIE_SECRET, USER_AGENT_COOKIE_SECRET | auth | cookies | generate |
PAR_REQUESTS_SECRET | auth | stored pushed authorization requests | generate |
DPOP_NONCES_SECRET | auth | DPoP nonces | generate |
PASSWORDS_SECRET | auth | key every stored password hash is computed with | first install only |
REFRESH_TOKENS_SECRET | auth | MAC key of every stored refresh token | first install only |
CLIENT_SECRETS_SECRET | auth, central | AES key for client secrets, signing keys, edge keys and resource secrets stored by central | first install only |
CENTRAL_SECRET_KEY | auth, central | key the two share | generate |
JWT_PRIVATE_KEY and JWKS_JSON | auth, central | token signing key (private half in auth, public JWK set in central) | first install only, as a group |
ACCOUNT_RESOURCE_SECRET | central | credential of the Account Settings listener | generate |
CENTRAL_RESOURCE_SECRET | central | credential of central’s admin API, stored encrypted by central at its first boot | first install only |
EDGE_PRIVATE_KEY, EDGE_KEY_ID and EDGE_PUBLIC_JWK | edge, central | edge’s key pair for calls to central; the public half is in central | first install only, as a group |
EDGE_TOKEN_ENC_KEY | edge | AES key of the refresh tokens in edge’s session table | first install only |
EDGE_SESSIONS_SECRET, EDGE_INTERNAL_SECRET, EDGE_DPOP_NONCE_SALT, EDGE_NATIVE_BLOB_KEY | edge | session, internal-call, DPoP nonce and native-blob values | generate |
POSTGRES_PASSWORD | auth, central, edge | the database role’s password, one value for all three on vps | first install only |
ADMIN_BOOTSTRAP_PASSWORD | auth | temporary password of the first admin user | generate |
UTILITY_CLIENT_PUBLIC_JWK (central) and UTILS_PRIVATE_KEY_JWK (utils) | central, utils | the utils client’s key pair | first install only, as a group |
On Kubernetes the Postgres password and the admin password are operator-supplied (external), and each service has its own POSTGRES_PASSWORD.
The schema
versola-tools writes a secrets.schema.json next to the configuration it generates. It is the single declarative list that versola-cli plans against; it contains names and rules, never values.
| Field | Meaning |
|---|---|
schemaVersion | Version of the file format (1). |
revision | Version of the schema’s content: which secrets it lists, counting from 1. One number for the whole file. |
name, services | Secret name and the services that receive it. A secret listed for several services is one value and must match everywhere. The same name in separate entries is separate values. |
type | base64url (with size in bytes), rsa-private-key, jwk-set, public-jwk, private-jwk, key-id or opaque. |
group | Secrets that only work as a set, such as a key pair split across services. All members are taken, or none. |
since | The revision in which the secret first appeared. |
onMissing | What to do when the store has no value: generate, generate-on-first-install-only or external. |
file | A file written instead of an env entry (the utils private key). |
onMissing values:
| Value | When the secret is absent from OpenBao |
|---|---|
generate | A new value is generated. Replacing it only signs users out or invalidates in-flight codes and requests. |
generate-on-first-install-only | Generated on a first install. On an upgrade it is an error, because something already depends on the real value: stored data encrypted with it, a key central already trusts, or a password the database role already has. |
external | Never generated: you must set it in OpenBao first. |
since and the deployment’s recorded revision are how the CLI tells a secret that is new in a release (not in the store because nothing could have put it there: generate it) from one that was lost (stop). The CLI records the schema revision it last applied for a deployment in state.json; a deployment with no record counts as revision 1.
What configure does for each secret
An existing value in OpenBao always wins. The policy only decides what happens when there is none. First configure concludes the state of the deployment from this machine’s record (~/.versola/active/state.json) and from what OpenBao holds:
| Record on this machine | OpenBao | State | Effect on generate-on-first-install-only secrets |
|---|---|---|---|
| none | empty | first install | generated |
| yes | has secrets | upgrade | generated only if new in this revision; otherwise stop (lost) |
| yes | empty | alarm | stop: OpenBao looks wiped, or is not the one this deployment used |
| none | has secrets | unknown | stop: new and lost cannot be told apart (another machine, a lost ~/.versola) |
| none, but an earlier first install did not finish | any | installing | handled as a first install; nothing has run on these secrets yet |
local target | any | local | generated: local is a throwaway stack |
Per secret the result is one of:
| Action | Meaning |
|---|---|
keep | OpenBao already holds a value. Nothing is written. |
create | A value is written: a freshly generated one, or, for a shared secret that one service has and another lacks, the stored copy. |
replace | Only for a key group left half-written by an interrupted first install (or new in this revision): the whole group is generated again. |
orphan | Stored but not in the schema. Reported, never removed. |
skip-utils | The utils key group, handled separately (see below). |
A problem stops the run with nothing written:
| Problem | Means |
|---|---|
lost | The secret existed at the deployed revision, OpenBao does not hold it, and stored data depends on it. Restore it from a backup. |
group-partial | Only some members of a group are stored. None is generated, because members only work as a set. |
shared-conflict | Services sharing one secret hold different values. Set the same correct value under each. |
unknown-state | No record on this machine, OpenBao not empty. |
vault-empty-with-state | A record exists, OpenBao holds nothing. |
external-missing | An external secret is not set. |
no-candidate, candidate-conflict, invalid-schema | The tools image and its schema disagree, or the schema uses a policy this CLI does not know; upgrade versola-cli. |
unknown-secret | A name given to --generate-missing is not in the schema, most likely a typo. |
POSTGRES_PASSWORD must be identical under auth, central and edge on vps; configure refuses to continue if they differ.
Reading a plan
versola secrets plan <target> <version> prints the same decisions without writing anything, and exits non-zero when there are problems.
versola secrets plan vps <version>
Secrets plan: vps, Versola <version>
Schema revision 2, deployed revision 1 — upgrade (a deployment record exists, OpenBao holds secrets)
auth ACCESS_TOKENS_SECRET keep
auth PASSWORDS_SECRET keep
edge EDGE_NATIVE_BLOB_KEY create (generated, new in this revision)
Result: OK
(Abridged: the real output has one line per secret.) With --json the same report is printed as one object. Secret values never appear in either format.
{
"version": "<version>",
"ok": false,
"target": "vps",
"state": "upgrade",
"schemaRevision": 2,
"deployedRevision": 1,
"actions": [
{ "service": "auth", "name": "ACCESS_TOKENS_SECRET", "action": "keep" },
{ "service": "edge", "name": "EDGE_NATIVE_BLOB_KEY", "action": "create", "source": "generated", "reason": "new-in-revision" }
],
"problems": [
{ "kind": "lost", "name": "PASSWORDS_SECRET", "services": ["auth"] }
]
}
reason is one of first-install, new-in-revision, policy-generate, forced-by-flag, fill-shared, local-throwaway or restart-install. source is generated or stored.
Generating what the plan refuses
When the plan stops because a secret looks lost, the right move is to restore it from a backup. If you know nothing stored depends on it, confirm the generation by name:
versola configure vps <version> --auth-url https://id.example.com --postgres-host 127.0.0.1:5432 --generate-missing EDGE_NATIVE_BLOB_KEY
- The flag takes a comma-separated list of names. It never makes a key group complete by itself: a group is generated only if every member is named.
- Generating a data-bound secret (
PASSWORDS_SECRET,REFRESH_TOKENS_SECRET,CLIENT_SECRETS_SECRET,EDGE_TOKEN_ENC_KEY) makes everything it protects unreadable: every login fails, or every refresh token or stored client secret becomes invalid. --allow-legacy-secretsis for a release whose tools image writes no schema. Then the CLI cannot tell new from lost and refuses onvpsunless OpenBao is empty and no deployment is recorded. With the flag it uses the old rule: anything missing is generated, lost or not.
Rotating a secret
An existing value always wins, so rotating means writing the new value into OpenBao and running configure again. Use bao kv patch, because kv put replaces the whole path:
docker exec -it -e BAO_ADDR=http://127.0.0.1:8200 -e BAO_TOKEN=<root token> versola-openbao-vps \
bao kv patch -mount=secret versola/vps/auth SESSIONS_SECRET='<new value>'
Then run versola configure vps <current-version> with your usual flags and versola up. Preview with versola secrets plan first.
What you can replace depends on the secret:
| Kind | Examples | Rotation |
|---|---|---|
generate | session, cookie, authorization code, DPoP nonce and PAR secrets, EDGE_SESSIONS_SECRET, EDGE_INTERNAL_SECRET | Replace freely. Users are signed out and in-flight codes and requests fail; they expire within minutes anyway. Check the effect of ACCESS_TOKENS_SECRET on issued tokens before you replace it. |
ADMIN_BOOTSTRAP_PASSWORD | A temporary credential: the first login with it forces a new password, so there is nothing to rotate afterwards. | |
| Data-bound | PASSWORDS_SECRET, REFRESH_TOKENS_SECRET, CLIENT_SECRETS_SECRET, EDGE_TOKEN_ENC_KEY | Do not replace. A new value does not rotate the old data, it destroys it (every password hash, refresh token, stored client secret or edge session). Versola does not provide a procedure to re-encrypt stored data under a new key. |
POSTGRES_PASSWORD | Change the role’s password in Postgres and the value under all three OpenBao paths together, then configure and up. | |
| Key pairs | JWT_PRIVATE_KEY/JWKS_JSON, edge key group, utils group | Replace as a whole group, never one half. central already trusts the stored public key: a fresh edge pair makes every edge-to-central call fail with 401. Signing-key rollover is done through the admin console’s JWKS, not by overwriting these values. |
CENTRAL_RESOURCE_SECRET | Cannot be rotated through the store. central stores its copy, encrypted, at its first boot and never reads the store again; a rotation through the admin API changes central’s copy, not OpenBao’s, so tools that present the stored value (cert-sync, operator scripts) get 401 (#380). On Kubernetes, set bootstrap.resource-secret before the first start. |
The utils key pair
central registers a confidential client named utils for tooling such as loadgen provision and versola-cli, authenticating with private_key_jwt and DPoP. Its key pair is split: the public half (UTILITY_CLIENT_PUBLIC_JWK) goes into central’s configuration, and the private half is stored at secret/versola/<target>/utils (field UTILITY_CLIENT_PRIVATE_JWK) and written to utils.private-key.jwk in the bundle.
central applies the public half on every boot, so a regenerated private key would not match it and would lock out whatever authenticates as utils. The CLI therefore handles this group itself (skip-utils in a plan): the private key already in OpenBao wins, and the public half given to central is derived from it. To reuse an existing key, store it under the utils path before the first configure, or, with the generator on Kubernetes, pass it as the UTILS_PRIVATE_KEY_JWK environment variable (the contents of utils.private-key.jwk). The key must be an EC P-256 private JWK.
A utils client that an older release seeded with a client_secret cannot be switched to a key: a client’s authentication method is fixed when it is created. Delete the utils client so the next boot registers it again, or keep the old bootstrap.utility-client.
Bootstrap seeds apply once
Values in central’s bootstrap block seed the database on its first start against an empty database: the central-admin client and preset, edges, the JWK set, the metadata document, the central resource secret and the utils client. Changing the configuration later does not change what is stored. Change those objects in the admin console, or, for the secrets above, follow the table in Rotating a secret.
The first admin login is separate: auth creates the admin user with the temporary ADMIN_BOOTSTRAP_PASSWORD, valid for 24 hours. Restarting auth after it expires sets a fresh one.
Next steps
- VPS deployment: provision OpenBao yourself and store AppRole credentials.
- CLI reference: flags of
secrets login,testandplan. - Configuration: where each secret enters the configuration files.