Secret/Vault API

< Secret
Revision as of 21:50, 21 August 2026 by Lchrisman (talk | contribs) (ER 22426: vault storage kinds and Secret::Caller enforcement shipped)
Release:

 • 4.6 •  5.0 •  5.1 •  5.2 •  5.3 •  5.4 •  6.0 •  6.1 •  6.2 •  6.3 •  6.4 •  6.5 •   •  6.6 •  7.0 •  7.1 •  7.2

new to Analytica 7.2

The vault-user and vault-shared storage kinds of Secret are live in Analytica 7.2: the engine-side vault client and the HTTP contract below shipped, and any vault that conforms to this contract works today. (A Lumina-hosted vault service for the Analytica Cloud Platform is a separate, future deliverable -- for now, deployments supply their own conforming vault.)

Overview

A vault is an external web service that stores secret values for an Analytica deployment. It matters most for server deployments (the Analytica Cloud Platform, or a custom web application hosting ADE), where "per-Windows-user storage" is meaningless because every browser user shares one service account: the vault stores each value keyed by the application's notion of who is logged in.

The division of labor is strict, and it is what keeps a vault simple to implement:

  • The vault is only ever asked: "the bytes stored under (identity, name, policy fingerprint)". It stores and returns opaque values.
  • All policy enforcement -- which sinks may substitute a secret, which destinations it may be sent to -- happens inside the Analytica engine, exactly as for every other storage kind. The vault never learns where a value is being sent and has no say in whether a substitution is allowed.
  • The policy fingerprint (a hash of the secret's security metadata) is part of every key. This preserves the freeze invariant of Secrets across any backend: a declaration whose policy has been edited asks for a key that was never written, and simply finds nothing. A vault cannot be tricked into releasing a value under a loosened policy.

A declaration opts into vault storage with Secret::Storage: vault-user (per-person values) or vault-shared (one value for the whole deployment), and names its vault with a Secret::Vault attribute when more than one vault is configured.

Configuring vaults

Vaults are declared in deployment configuration. More than one may be configured, each with a name.

On an Analytica Cloud Platform server, in the Suan .config file:

SecretVault acme     = https://secrets.acme.com/analytica/v1
SecretVaultAuth acme = %ACME_VAULT_TOKEN%
SecretVault gmaps    = https://config.acme.com/mapskey/v1

(The %ENVVAR% form is resolved from the environment at request time, keeping the token out of the config file. Both the auth token and the signing key accept it.)

Anywhere else (desktop Analytica, ADE), via a system function in Analytica.ini:

SysLib_Internal::AddSecretVault("https://secrets.acme.com/analytica/v1",
                                "%ACME_VAULT_TOKEN%", name: "acme")

(In Analytica.ini the call must be written without a leading :: -- root-qualified names do not resolve during startup-file reads. From typescript or a button script, the root-qualified spelling ::SysLib_Internal::AddSecretVault works too.)

Registration rules:

  • Vaults registered at startup (from Analytica.ini or the Suan config) live for the process. A model may also call ::SysLib_Internal::AddSecretVault itself -- permitted, though not the expected pattern -- in which case the vault is retired when that model closes.
  • Model-registered vaults are read-only: the engine will GET from them but never PUT or DELETE. This closes a trap where a malicious model could register its own vault and harvest whatever a user types into a credential prompt. Startup-registered vaults have full read/write.
  • The call is an error where side effects are disallowed (i.e. during a variable evaluation). Button scripts, typescript, and Analytica.ini itself are the sanctioned contexts.
  • Vault configuration and use is available in every edition, including Free -- vaults serve the running side of models, and gating them would break exactly the distribute-to-anyone deployments they exist for.

The HTTP contract

All requests go to the configured base URL over https (with one narrow exception: plain http is accepted for loopback addresses, for local testing), carrying:

Authorization: Bearer «token from the config»

Path segments are URL-encoded. {identity} is the session's user identity for vault-user secrets, and the literal - for vault-shared. {policyFp} is the secret's policy fingerprint.

GET    {base}/secret/{identity}/{qualifiedName}/{policyFp}
       -> 200 {"value":"<base64>"}   or 404 (not set)   or 403 (denied)

PUT    {base}/secret/{identity}/{qualifiedName}/{policyFp}     (optional capability)
       body {"value":"<base64>"}  -> 204

DELETE {base}/secret/{identity}/{qualifiedName}/{policyFp}     (optional capability)
       -> 204

Semantics a vault implementor should honor:

  • PUT and DELETE are optional. A read-only vault (deployment-provisioned keys) answers them with 405, which the engine surfaces to the user as "this secret cannot be set here -- contact your administrator."
  • Values are opaque bytes (base64 in transit). The vault must store them encrypted at rest and must never log them. The engine likewise never logs response bodies.
  • Failure is unavailability. Any other status, a TLS failure, or a timeout makes the secret cleanly unavailable (the resulting error names the vault alias). The engine never falls back to a different store.
  • The engine may cache a fetched value in process memory for the session (scrubbed on close). A 404 on a vault-user secret triggers the masked credential prompt in the client, and a successful set invalidates any cached miss. A 404 on a vault-shared secret is an operator error -- the session gets a clean failure, and no end user is ever prompted to supply a deployment-wide secret.

Signed identity assertions

With only a bearer token, any caller holding the token could GET with any {identity} segment. A vault that wants identity to be unspoofable by mere possession of the transport credential can require signed identity assertions.

Configuration adds a signing secret, distinct from the bearer token -- SecretVaultSign acme = %ACME_VAULT_SIGNING_KEY% in the Suan config, or a sign: parameter to AddSecretVault. Its presence makes the engine attach one extra header to every request:

X-Analytica-Identity: v1.hmac256.«base64url(payload)».«base64url(mac)»

where the payload is JSON:

{ "sub": "u123",                                  -- the asserted identity
  "aud": "https://secrets.acme.com/analytica/v1", -- this vault
  "req": "GET /secret/u123/AcmeLib::CrmToken/3fa2...",
  "iat": 1755640000, "exp": 1755640300,           -- at most a 5-minute window
  "jti": "9f27c..." }                             -- nonce

and mac = HMAC-SHA256(signingKey, payload).

Verification, on the vault side: recompute the MAC; check that aud is this vault, that req matches the actual method and the full request path (including any path prefix of the base URL), that sub matches the path's identity segment, and that iat/exp bound the window. Optionally track jti within the window to eliminate replays outright; even without that, a replay can only repeat the identical idempotent request for a few minutes. A vault configured to require assertions answers unsigned requests with 401.

The signing key lives only in deployment configuration -- never in the language, never in a model.

Trust model

The vault trusts the deployment's bearer token, and -- within it -- the engine's assertion of {identity} (strengthened by signed assertions where configured). Tokens should be scoped per deployment. The engine-side guarantees (Secret policy enforcement, fingerprint keying) do not depend on the vault's honesty: a malicious vault could at worst disclose the values it was itself given to hold, which is true of any credential store.

See Also

Comments


You are not allowed to post comments.