Secrets — Versola Docs
VersolaVersola/docs
Pre-1.00.7.1versola.kzGitHub

Secrets

Which secrets Versola uses, how versola-cli stores them in OpenBao, what configure and secrets plan decide for each one, and which secrets can and cannot be rotated.

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

  • local and vps targets. In OpenBao (a Vault-compatible secret store, KV v2, mounted at secret), at secret/versola/<target>/<path>, where <path> is one of auth, central, edge and utils. versola configure starts 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.env files (mode 600) in the deployment bundle. The services never talk to OpenBao at runtime; OpenBao has to be up only while you run configure.
  • 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 pathHolds
secret/versola/<target>/authSecrets loaded into the auth container.
secret/versola/<target>/centralSecrets loaded into central, including the public halves of key pairs.
secret/versola/<target>/edgeSecrets loaded into edge.
secret/versola/<target>/utilsThe 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.

SecretServiceProtectsOn missing
ACCESS_TOKENS_SECRETauthaccess token keygenerate
AUTH_CODES_SECRETauthauthorization codesgenerate
SESSIONS_SECRETauthSSO sessionsgenerate
CONVERSATION_COOKIE_SECRET, SESSION_COOKIE_SECRET, USER_AGENT_COOKIE_SECRETauthcookiesgenerate
PAR_REQUESTS_SECRETauthstored pushed authorization requestsgenerate
DPOP_NONCES_SECRETauthDPoP noncesgenerate
PASSWORDS_SECRETauthkey every stored password hash is computed withfirst install only
REFRESH_TOKENS_SECRETauthMAC key of every stored refresh tokenfirst install only
CLIENT_SECRETS_SECRETauth, centralAES key for client secrets, signing keys, edge keys and resource secrets stored by centralfirst install only
CENTRAL_SECRET_KEYauth, centralkey the two sharegenerate
JWT_PRIVATE_KEY and JWKS_JSONauth, centraltoken signing key (private half in auth, public JWK set in central)first install only, as a group
ACCOUNT_RESOURCE_SECRETcentralcredential of the Account Settings listenergenerate
CENTRAL_RESOURCE_SECRETcentralcredential of central’s admin API, stored encrypted by central at its first bootfirst install only
EDGE_PRIVATE_KEY, EDGE_KEY_ID and EDGE_PUBLIC_JWKedge, centraledge’s key pair for calls to central; the public half is in centralfirst install only, as a group
EDGE_TOKEN_ENC_KEYedgeAES key of the refresh tokens in edge’s session tablefirst install only
EDGE_SESSIONS_SECRET, EDGE_INTERNAL_SECRET, EDGE_DPOP_NONCE_SALT, EDGE_NATIVE_BLOB_KEYedgesession, internal-call, DPoP nonce and native-blob valuesgenerate
POSTGRES_PASSWORDauth, central, edgethe database role’s password, one value for all three on vpsfirst install only
ADMIN_BOOTSTRAP_PASSWORDauthtemporary password of the first admin usergenerate
UTILITY_CLIENT_PUBLIC_JWK (central) and UTILS_PRIVATE_KEY_JWK (utils)central, utilsthe utils client’s key pairfirst 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.

FieldMeaning
schemaVersionVersion of the file format (1).
revisionVersion of the schema’s content: which secrets it lists, counting from 1. One number for the whole file.
name, servicesSecret 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.
typebase64url (with size in bytes), rsa-private-key, jwk-set, public-jwk, private-jwk, key-id or opaque.
groupSecrets that only work as a set, such as a key pair split across services. All members are taken, or none.
sinceThe revision in which the secret first appeared.
onMissingWhat to do when the store has no value: generate, generate-on-first-install-only or external.
fileA file written instead of an env entry (the utils private key).

onMissing values:

ValueWhen the secret is absent from OpenBao
generateA new value is generated. Replacing it only signs users out or invalidates in-flight codes and requests.
generate-on-first-install-onlyGenerated 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.
externalNever 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 machineOpenBaoStateEffect on generate-on-first-install-only secrets
noneemptyfirst installgenerated
yeshas secretsupgradegenerated only if new in this revision; otherwise stop (lost)
yesemptyalarmstop: OpenBao looks wiped, or is not the one this deployment used
nonehas secretsunknownstop: new and lost cannot be told apart (another machine, a lost ~/.versola)
none, but an earlier first install did not finishanyinstallinghandled as a first install; nothing has run on these secrets yet
local targetanylocalgenerated: local is a throwaway stack

Per secret the result is one of:

ActionMeaning
keepOpenBao already holds a value. Nothing is written.
createA value is written: a freshly generated one, or, for a shared secret that one service has and another lacks, the stored copy.
replaceOnly for a key group left half-written by an interrupted first install (or new in this revision): the whole group is generated again.
orphanStored but not in the schema. Reported, never removed.
skip-utilsThe utils key group, handled separately (see below).

A problem stops the run with nothing written:

ProblemMeans
lostThe secret existed at the deployed revision, OpenBao does not hold it, and stored data depends on it. Restore it from a backup.
group-partialOnly some members of a group are stored. None is generated, because members only work as a set.
shared-conflictServices sharing one secret hold different values. Set the same correct value under each.
unknown-stateNo record on this machine, OpenBao not empty.
vault-empty-with-stateA record exists, OpenBao holds nothing.
external-missingAn external secret is not set.
no-candidate, candidate-conflict, invalid-schemaThe tools image and its schema disagree, or the schema uses a policy this CLI does not know; upgrade versola-cli.
unknown-secretA 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-secrets is for a release whose tools image writes no schema. Then the CLI cannot tell new from lost and refuses on vps unless 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:

KindExamplesRotation
generatesession, cookie, authorization code, DPoP nonce and PAR secrets, EDGE_SESSIONS_SECRET, EDGE_INTERNAL_SECRETReplace 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_PASSWORDA temporary credential: the first login with it forces a new password, so there is nothing to rotate afterwards.
Data-boundPASSWORDS_SECRET, REFRESH_TOKENS_SECRET, CLIENT_SECRETS_SECRET, EDGE_TOKEN_ENC_KEYDo 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_PASSWORDChange the role’s password in Postgres and the value under all three OpenBao paths together, then configure and up.
Key pairsJWT_PRIVATE_KEY/JWKS_JSON, edge key group, utils groupReplace 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_SECRETCannot 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