Lockstep

Using the tool

1. Choose what to compare

Each side (left in blue, right in orange) needs:

Both sides can be the same Vault with different mounts or prefixes. ⇄ swaps the sides.

Or an export file. Switch a side from Vault to Export file and open a secret-engine export (with its key if it's encrypted) to compare a Vault against it: a backup, or another environment's export. Show values reads the file's values in the page. An export is read-only: you can copy values from it into a Vault, never into it, and it has no version history or value inspector. It's held in the page's memory only and is gone when you reload.

2. Read the comparison

Press Compare. The server lists both trees, reads every secret on both sides, compares them, and returns only statuses: no values. They're dropped as soon as each secret has been compared.

Status Meaning
Equal Same keys, same values
Different At least one key differs, or exists on one side only
Left only / Right only The secret exists on one side only (a soft-deleted latest version counts as absent, shown as deleted)
No access One side can't list or read it. Its keys show as unknown, never as "only on the other side", so nobody copies over data they couldn't see
Error Vault returned something else; the message is shown

What counts as a difference:

Filters (All changes, Different, Left only…, Everything) and the path/key search narrow the list. Folders a side couldn't list are named above the results.

Values and metadata

Comparison profiles

A profile tells Lockstep how the two sides relate, when they aren't laid out identically. Choose one in the Comparison profile bar, then compare.

A profile pairing prod paths with staging paths, with the host key ignored

Rule Example Effect
Path mapping services/*/prod/** ⇄ services/*/staging/** Pairs services/billing/prod/db (left) with services/billing/staging/db (right). The row shows both paths
Ignored key host under services/** host is still shown, marked ignored, but its difference doesn't make the secret Different
Ignored path legacy/** Matching secrets are left out of the comparison

How the rules work: - Patterns: * matches within one path segment and ** across any depth. Both sides of a mapping need the same wildcards in the same order, and what they match is carried across. The first matching mapping wins. Paths no mapping matches are compared as they are. - Copies follow the mapping. Copying a left-only services/search/prod/config to the right writes services/search/staging/config. - Collisions are refused, not guessed. If two left paths map onto the same right path, that pair shows as an Error explaining which paths collide. - Mounts too: a profile can also remember each side's mount and prefix, applied when you pick it. - The summary line says how many pairs a mapping matched, and how many key differences and paths the profile ignored.

Shared and personal profiles: - Shared profiles come from the server (LOCKSTEP_PROFILES, see configuration), so a team sees the same rules. They're read-only: Duplicate makes an editable copy. - Personal profiles are saved in your browser. New and Edit open the editor; Export and Import move them between browsers or people as JSON.

Building a profile from a comparison

You don't need to write patterns to ignore things. Compare, then press Pick what to ignore…:

Each pick is struck through straight away and listed in the bar above the results. Pressing it again undoes it. Compare with these rules runs the comparison with your picks on top of the selected profile, so you can check the effect before keeping them. Save as profile… opens the profile editor with the rules filled in: name it and save. If a personal profile is selected, the button reads Add to “name”… and adds the picks to it. With a shared profile, it saves a copy. Unsaved picks are kept until you save or Discard them.

Ignore mode: a secret, a folder and a key picked, listed as unsaved rules

3. Copy values between the sides

Override values

To set a value yourself instead of copying the other side's, expand the secret and choose Override left, Override right or Override both. Tick the keys to set and type their new values; + Add key adds a key the secret doesn't have yet. Values are text unless you tick JSON, which stores numbers, booleans, objects and lists as those types. While Show values is off, what you type is masked; with it on, the fields start from the side's current values.

Override editor setting a new password on one side

Stage override adds the values to the changeset, marked set → or ← set on each key, and they go through the same review and apply as copies: check-and-set against the version you compared, other keys kept, nothing deleted. An override and copies into the same secret are written together, as one new version. A key is either copied or overridden on a side, not both: staging one replaces the other.

Override values are the only values the browser sends to the server. They're kept in the page until you apply or clear them, are never stored, and aren't audit-logged: the log lists the overridden key names only. Because sync plans never hold values, a review with overrides can't be saved as a plan.

Review & apply opens a preview for each target secret: keys added or updated (with old → new values if Show values is on), the version it will become, and any problems:

Preview status Meaning
Will be written Ready
Already the same Nothing to do
Changed since compare The target's version moved since you compared. Compare again
No permission The target token can't write there (sys/capabilities-self)
Can't copy The source no longer has that key or data

Tick the confirmation (it names the target Vaults and mounts) and Apply. Then:

Results are shown for each secret, then the tool compares again.

Audit log

Every attempted write, applied or not, produces one JSON line on the server's stdout:

{"event": "sync.apply", "outcome": "applied", "ts": "2026-11-08T12:00:00Z",
 "actor": {"source_identity": "oidc-alice@example.com", "source_auth": "oidc",
           "target_identity": "oidc-alice@example.com", "target_auth": "oidc"},
 "source": {"vault": "Vault B", "url": "http://vault-b:8200", "mount": "app-config", "path": "frontend/settings", "version": 8},
 "target": {"vault": "Vault A", "url": "http://vault-a:8200", "mount": "app-config", "path": "frontend/settings",
            "old_version": 8, "new_version": 9},
 "keys": ["cdn_url"], "overridden": [], "message": ""}

Values are never logged: keys lists every key written and overridden the ones set from typed values. Outcomes are applied, conflict, denied, invalid and error.

Sync plans

A plan is a reviewed set of copies saved as a file, so one person can prepare the changes and another (with write access to the targets) can apply them later. It's also a report of what needs updating from where.

Save a plan. Stage copies as usual and open Review & apply…. Add an optional note and choose Save as plan. You don't need write access to the targets to do this: a read-only user can prepare a plan for someone else. Lockstep downloads lockstep-plan-<date>-<id>.json; Download report gives the same plan as a Markdown table (from, to, key, add or update). Changes that conflict or can't be read can't go into a plan: compare again first. Keys whose target already has the same value are left out.

A plan records, for each secret: the source and target paths, the keys and whether each is added or updated, and the version of the source and target at the time. It never contains values: they're read from the source when the plan is applied. It also records who created it on each side, the comparison profile (if any) and the note. The file is signed by the Lockstep server (with a key derived from LOCKSTEP_SESSION_KEY), so any edit to it is detected and the plan is refused. A plan made by one Lockstep server can't be applied on another with a different session key, and changing the session key invalidates existing plans.

Apply a plan. Connect both sides to the Vaults the plan names (any identity with write access to the targets), open the Sync plans tab and choose Open a plan file…. Check the report, then Check plan: Lockstep reads every source and target now.

The check runs again when you apply. If a secret changes in the moment between that check and its write, that one write is refused (check-and-set) and the result says so.

4. Value tools

Inspect (under the status of a key with a badge) opens that key's value from one or both sides. The value is identified by content, not by key name, through as many layers as it has: base64 → PEM → certificates, base64 → DER, and so on.

Detected What you get
X.509 certificate, PEM bundle, PKCS#7, DER Subject, issuer, names (SANs), validity, key usage, key type, serial, fingerprints. Chain validation link by link: signature, issuer is a CA, dates. The verdict is trusted, untrusted root, incomplete or invalid
PKCS#12 Opens with a typed password, one taken from another key in the same secret (keys named like *pass* are suggested), or one the password finder locates elsewhere. Lists the private key and certificates, and checks the key matches its certificate
JKS / JCEKS Certificates are listed without a password. With it, the keystore's integrity is verified and JKS private keys are decrypted and matched to certificates. JCEKS private keys are shown as present only
Private key, encrypted key, public key, CSR Type and size; which certificate (in this value or elsewhere in the secret) the key belongs to
PNG, JPEG, GIF, WebP, ICO, SVG Preview. SVGs are shown as images, so scripts inside them never run
ZIP / JAR, GZIP, PDF, other binary File list (ZIP), hex preview, download

A JKS keystore opened with its password: integrity verified, private key decrypted and matched to its certificate

Chains. Intermediates are taken from the value itself and from the secret's other keys (chain.pem, ca.crt…). Trust anchors are the system roots (Mozilla's list), any certificate key from the comparison you tick, or Trust this root, offered for a self-signed root found alongside the value. Anchor choices apply to that inspection only and aren't saved.

Both sides. When a key exists on both sides, Inspect opens them in two columns and compares their first certificates field by field. For example, a renewed certificate shows the same subject and a new serial and expiry.

Previews and downloads need Show values. Private keys inside keystores can't be downloaded. Keystore passwords are used for that one request only.

Finding a keystore password

Keystore passwords often live away from the keystore, for example some-secrets/files/certs.p12 with its password in some-secrets/strings/KEYSTORE_PASSWORD. When the inspector opens a locked keystore, it looks for the password straight away:

The password finder offering the key that opens the keystore

  1. It searches the keystore's top-level folder (some-secrets/ here; the whole mount for a secret at the root), up to 500 secrets.
  2. Candidates are short, single-line values whose key name or secret name looks like a password (*pass*, *pwd*, *pin*, *secret*), or that sit in a folder named like one (passwords/, creds/…).
  3. They're ranked by closeness (same secret first, then the most folders in common) and by shared name words, so CERT_PASSWORD ranks high for certs.p12.
  4. The server tries the 40 best against the keystore. Ones that open it are listed first with Use; the others are listed as tried. If none does, Java's default password (changeit) is tried too.

Only where each candidate is (path and key) and whether it opens the keystore come back, never the values, and nothing is written. You need read access to the secrets searched; folders you can't list are skipped.

5. Certificate expiry

The Certificate expiry tab scans the mounts (and prefixes) chosen on one or both sides for every certificate stored anywhere: PEM certificates and bundles, DER, base64-wrapped forms, PKCS#12 files, and JKS/JCEKS keystores. All are found by content, not key name.

Certificate expiry dashboard

Keystores that need a password are opened with a password stored in the same secret (keys named like *pass*, *pwd*), or else with the 10 best candidates the password finder ranks from the keystore's top-level folder within the scanned tree: keys you can already read. The row says which key opened it. Keystores that can't be fully checked are listed under not fully checked, never silently skipped. Only certificate details come back from the server, never secret values.

6. Export and import

The Export & import tab moves a whole secret engine, values included, through a file: for a backup, a migration, a copy into an environment you can't reach from here, or to compare against later.

Export

  1. Export from: the left or right side's Vault, mount and prefix.
  2. Leave out secrets (globs such as legacy/**) and keys (a key pattern under a path glob), or start from a comparison profile's ignore rules.
  3. Encrypt with a one-time key is on by default. Export and download saves the file and shows the key once: copy it, keep it apart from the file, then hide it. It's never stored, so without it the file can't be opened. Unencrypted exports hold every value in plain text, and the tool says so before you export one.

The file (lockstep-export-<vault>-<mount>-<prefix>-<time>[.encrypted].json) holds each secret's current data, version and update time, plus where it came from, the rules used, and what was left out or couldn't be read. Its format is described by a JSON Schema.

Encryption: AES-256-GCM with a random 256-bit key, the content zlib-compressed. The key looks like lsk1. followed by 43 characters. The envelope's header (Vault name, mount, prefix, number of secrets, time) stays readable so files can be told apart, and is authenticated: changing it makes the file fail to open.

Exporting reads every secret with your token, so you only export what you can read. Each export is audit-logged ("event": "export": who, from where, how many secrets, whether encrypted, the rules), never with values. Admins can allow only encrypted exports, or turn exporting off, with LOCKSTEP_EXPORT_MODE (configuration).

Import

  1. File: open an export (or pick one already opened in the page).
  2. Target: the left or right side's Vault, mount and prefix. Exported paths are written under it.
  3. Rules (optional), applied in this order: - Leave out secrets and keys, as when exporting. - Transforms, applied in order: Replace text, Add prefix, Add suffix, UPPERCASE, lowercase, on values (text values only), key names or paths. Each can be limited to paths matching a glob, and to keys matching a pattern. For example, replace application-dev-env with application-test-env in values, devDBA with testDBA, and dev/ with test/ in paths. A transform's path filter sees the path as earlier transforms left it. - Save rules downloads them as JSON (schema); Load rules reads them back, and lockstep import --rules takes the same file.
  4. Delete what isn't in the file (optional): make the target match the file. See below.
  5. Preview import shows what the rules did (left out, changed) and, per target secret, the keys to add, update or remove and the version it will become, and any secrets it will delete. Secrets that two paths (or keys) would collide on are left out with the reason, never merged by guesswork.
  6. Tick the confirmation and Import.

Importing writes like copying: values are merged into each target secret (keys only on the target are kept, nothing is deleted), with check-and-set against the version the preview saw, so a secret changed in the meantime is reported as a conflict, not overwritten. Every write is audit-logged without values, with "source_auth": "file".

Deleting what isn't in the file

With Make the target match the file ticked (lockstep import --delete-missing), the import treats the file as the whole truth for the target's mount and prefix: - each imported secret is written with only the file's keys, so keys only on the target are removed (the new version replaces the old one, which stays in the version history); - secrets under the target's prefix that aren't in the file are soft-deleted: their latest version is deleted, and vault kv undelete restores it. Nothing is destroyed.

To protect a secret, list it in the file with "keep": true and no data. It's left exactly as it is: neither written nor deleted. Keep entries follow path transforms, like the other paths in the file:

"secrets": [
  {"path": "payments/db", "version": 4, "data": {"user": "app", "password": "…"}},
  {"path": "payments/legacy-api", "keep": true}
]

Only what the preview listed is deleted, and only at the version the preview saw: a secret changed in the meantime is a conflict, and one created since the preview is left alone. Secrets left out by Leave out rules count as not in the file, so they're deleted too unless kept. Folders the token can't list are reported and nothing in them is deleted. If the target holds more secrets than Lockstep lists in one go (LOCKSTEP_MAX_TREE_SECRETS), the preview refuses rather than guess: import under a narrower prefix. Deleting needs the delete capability on <mount>/data/…. Every delete is audit-logged with "action": "delete".

Import panel with prod to staging transforms and a preview of the secret to write

View or edit this page on GitHub