Secret/Vault API
| 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 |
|---|
This page is a forward-looking specification. The vault-user and vault-shared storage kinds of Secret are not yet available in Analytica 7.2.0 -- external vault support is a later phase of the Secrets feature. The contract below is published in advance for the rare reader planning to implement a vault, and details may change before the capability ships. Everything else about Secrets works as documented on the main page.
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 reads the token from the server's environment, keeping it out of the config file.)
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")
Registration rules:
- Vaults registered at startup (from
Analytica.inior the Suan config) live for the process. A model may also call::SysLib_Internal::AddSecretVaultitself -- 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.iniitself 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-usersecret triggers the masked credential prompt in the client, and a successful set invalidates any cached miss. A 404 on avault-sharedsecret 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 path, 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.
Enable comment auto-refresher