PLUTUS · ENGINE ROOM // PAPER ONLY · NO LIVE CAPITAL
ENGINEERED BY LEOPARD DATA

Secrets & Identity

One configuration pipeline that resolves to user secrets on a laptop and to a managed identity on the VM, a blob client with no connection string at all, and an API key that cannot reach a log line. This page describes where secrets live. It prints none.

One pipeline, two environments

Every console builds its configuration the same way, in the same order, and later sources override earlier ones:

new ConfigurationBuilder()
    .AddJsonFile("appsettings.json", optional: false)
    .AddJsonFile("appsettings.Development.json", optional: true)
    .AddUserSecrets(typeof(Program).Assembly, optional: true)
    .AddEnvironmentVariables()
    .AddPlutusKeyVault()   // no-op without KEY_VAULT_NAME; the VM sets it
    .Build();

On a laptop the last line contributes nothing and user secrets win. On the trader VM, KEY_VAULT_NAME is set, Key Vault is added last, and it wins. The code does not know which machine it is on, and that is the property the local-first posture depends on: the one thing most likely to break between laptop and cloud is exercised by the same line in both places.

Key Vault — names, not values

Plutus.KeyVault is two small classes. AddPlutusKeyVault reads the environment variable, returns the builder untouched if it is empty, and otherwise attaches Azure Key Vault with a DefaultAzureCredential. PlutusKeyVaultSecretManager maps a vault secret's name to the configuration key the rest of the code reads:

Vault secretConfiguration key
fmp-api-keyFmpApiSettings:ApiKey
anthropic-api-keyClaude:ApiKey
database-connection-stringConnectionStrings:DefaultConnection
sendgrid-api-key, twilio-account-sid, twilio-auth-tokenConnectionStrings:…
ibkr-gateway-credentialsIbkr:GatewayCredentials

The vault itself is part of the estate — plutus-prod-kv, soft delete for 90 days, purge protection on — and it does not exist yet. When it does, the deployer can get, list, set and delete; the VM, the two sites and the function app can only get and list. Third-party keys enter it through pulumi config set --secret, which encrypts them in the stack file, and the function app reads them through @Microsoft.KeyVault(…) references rather than copies. Two GMI credentials, gmi-api-key and gmi-export-sas, will join the map when the ingest is built.

Blob — no connection string, nothing to rotate

AddPlutusArtifactStore constructs a single BlobServiceClient from an account URL and a DefaultAzureCredential, and that is the whole authentication story. On a developer machine the credential resolves to az login; on the trader VM it resolves to the machine's system-assigned managed identity. The storage account grants Storage Blob Data Contributor to both — the deployer's role is in the foundation, the VM's arrives with the estate.

There is no account key in the code, none in user secrets, none in an environment variable. There is nothing to leak and nothing to rotate, and a laptop and the VM run byte-identical storage code. The blob spec's §11 puts it as a rule: managed identity, never connection strings.

One honest footnote. The Pulumi program still materialises a blob-storage-connection-string secret for the function app, from an older pattern. The archiver and the backtester do not read it, and it should go when the function app moves to the same identity path.

The key never reaches a log

FMP authenticates with the key as a query-string parameter, which means every request URL contains it — and a request URL is exactly what a developer wants in an exception when a call fails. FmpApiClient resolves that with one line:

/// <summary>Never let the API key reach a log line or an exception message.</summary>
private string RedactKey(string url) => url.Replace(_apiKey, "***", StringComparison.Ordinal);

A non-success response throws FmpApiException carrying the status code, the redacted URL and the body. The retry loop logs status codes and delays, never URLs. The 402 messages quoted on the FMP page came out of that exception, which is why they can be quoted at all.

Local development — user secrets

Each console project — the trading daemon, the news archiver, the backtester — declares its own UserSecretsId in its project file. The .NET user-secrets tool stores values for that id under ~/.microsoft/usersecrets/, outside the repository, and AddUserSecrets reads them back at startup. The FMP key and the development database connection live there and nowhere else; the repository contains the ids, never the values.

If the key is missing, the archiver refuses to start with a message that names the exact dotnet user-secrets set command to run — or the environment variable to set on the VM. A missing secret is a loud failure at startup, not a 401 twenty minutes into a backfill.

Where each secret lives
laptop ~/.microsoft/usersecrets VM plutus-prod-kv via identity blob no secret — RBAC only repo ids only logs redacted
Never in the repository
  • An API key, for any vendor
  • A database connection string
  • A storage account key
  • An SSH private key — the VM takes a public key only, password auth disabled
  • A secrets.json of any kind