Skip to content

Managed secrets

Managed secrets keep outbound credentials out of C# source, function arguments, ordinary records, results, and run history. An owner or administrator writes a string value under a stable workspace key. Trusted C# business logic resolves that key through ISecretService only while it runs.

The value is accepted only when a secret is created or rotated. List, detail, audit, error, export, and UI responses contain metadata only. There is no reveal, copy-existing-value, rename, or restore-after-retirement operation.

Keys are immutable, workspace-unique, and case-insensitive. Moltaro does not trim or otherwise transform a key or value. A key with leading or trailing whitespace or a null character is rejected, and a value is stored exactly as submitted.

Workspace owners and administrators can complete the full lifecycle without handling raw API requests:

  1. Open Administration > Secrets.
  2. Choose Create secret, enter the immutable lookup key, and enter the initial value.
  3. Select a row to open its metadata and key-local Audit Trail history.
  4. Use Rotate value to replace the write-only value, or disable and re-enable the key without changing its value version.
  5. Use Retire only when the key must never be restored or reused. Retirement removes the ciphertext from the live secret row.

Retirement is terminal inside Moltaro, but it does not rewrite existing database backups, PostgreSQL WAL archives, or infrastructure snapshots. Those may retain older encrypted bytes until the operator’s normal retention and disposal policy expires them. Protect and retire the persistent Data Protection key ring under the same operational policy.

The create and rotate dialogs accept a value but never preload, reveal, or copy the current value. Closing and reopening a dialog always starts with an empty value field. The list, mobile cards, detail drawer, and history show metadata only. A Configurator or ordinary user cannot see the navigation item and is blocked from the direct route.

Use the local history to confirm who changed a key and when. Resolution events from Jobs, Commands, Actions, triggers, validations, before-save mutations, and HTTP endpoints appear in the same history after their invocation audit is flushed.

Automate the owner and administrator lifecycle

Section titled “Automate the owner and administrator lifecycle”

Only workspace owners and administrators can use the managed-secret Configuration API. A Configurator role is not sufficient. All paths below are relative to the configured installation’s workspace API base URL, never moltaro.com.

OperationRequestResult
List metadataGET /api/workspace/admin/secretsKeys, lifecycle state, version, timestamps, actors, and RowVersion
Read metadataGET /api/workspace/admin/secrets/{id}One metadata record; never its value
CreatePOST /api/workspace/admin/secrets with Key and ValueEnabled secret at value version 1
RotatePOST /api/workspace/admin/secrets/{id}/rotate with Value and RowVersionReplaces the value and increments its value version
DisablePOST /api/workspace/admin/secrets/{id}/disable with RowVersionPrevents resolution without changing the value version
Re-enablePOST /api/workspace/admin/secrets/{id}/enable with RowVersionMakes the current value resolvable again
RetirePOST /api/workspace/admin/secrets/{id}/retire with RowVersionTerminal state; increments the version and removes ciphertext from the live row

Keys are limited to 128 characters. Create and rotate require a non-empty value whose UTF-8 representation is no larger than 64 KiB; values over that limit are rejected before protection or storage.

Use the latest RowVersion returned by a read or mutation. HTTP 409 means another actor changed the record: read it again, decide whether the requested transition is still appropriate, and never resend a stale token blindly.

Separate keys rotate independently. Moltaro does not provide an atomic multi-key rotation. Design integrations so one credential can change at a time, or let the external service accept an overlap during rotation.

Create, rotate, disable, enable, retire, and runtime resolution events are written to the global Audit Trail with object type ManagedSecret. The same append-only events form a key’s local history; there is no separate mutable history store.

GET /api/workspace/admin/audit/events?objectType=ManagedSecret&objectId={secretId}&page=1&pageSize=50

Audit data contains the key, version, invocation identity, and stable outcome where applicable. It never contains the value, ciphertext, a value-derived hint, or a derived bearer token.

Inject Moltaro.Package.NET.Functions.ISecretService into a function constructor. GetRequired is appropriate when the integration cannot run without the setting. TryGet is for genuinely optional configuration.

The following schedule-compatible Job obtains short-lived authorization from an example external service. The URLs are deliberately non-production. The example sends credentials only to their intended token endpoint, keeps the derived token in memory for one request, and returns only non-secret status.

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Text.Json.Nodes;
using Moltaro.Package.NET;
using Moltaro.Package.NET.Functions;
namespace Moltaro.Operational.Functions;
[MoltaroFunction(
"docs.examples.managedSecretsPartnerImport",
Key = "docs.managedSecrets.partnerImport",
Name = "Partner import",
Description = "Authenticates with managed credentials and runs a partner import.")]
public sealed class ManagedSecretsPartnerImportJob(ISecretService secrets)
: GlobalFunction
{
public override async Task<FunctionResult> OnRunAsync(
CancellationToken cancellationToken)
{
var clientId = secrets.GetRequired("integrations.partner.client-id");
var clientSecret = secrets.GetRequired("integrations.partner.client-secret");
var tokenFields = new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_id"] = clientId,
["client_secret"] = clientSecret
};
if (secrets.TryGet("integrations.partner.scope", out var scope))
{
tokenFields["scope"] = scope;
}
using var client = new HttpClient();
using var tokenRequest = new HttpRequestMessage(
HttpMethod.Post,
"https://api.example.invalid/oauth/token")
{
Content = new FormUrlEncodedContent(tokenFields)
};
using var tokenResponse = await client.SendAsync(tokenRequest, cancellationToken);
if (!tokenResponse.IsSuccessStatusCode)
{
return FunctionResult.Failed("Partner authentication failed.");
}
var tokenPayload = await tokenResponse.Content
.ReadFromJsonAsync<JsonObject>(cancellationToken);
var accessToken = tokenPayload?["access_token"]?.GetValue<string>();
if (string.IsNullOrWhiteSpace(accessToken))
{
return FunctionResult.Failed("Partner authentication returned no token.");
}
using var importRequest = new HttpRequestMessage(
HttpMethod.Post,
"https://api.example.invalid/import");
importRequest.Headers.Authorization = new AuthenticationHeaderValue(
"Bearer",
accessToken);
using var importResponse = await client.SendAsync(importRequest, cancellationToken);
return importResponse.IsSuccessStatusCode
? FunctionResult.Ok("Partner import completed.")
: FunctionResult.Failed("Partner import failed.");
}
}

Do not include the credentials or accessToken in DevLog data, exception text, function results, correlation ids, request diagnostics, audit data, or ordinary records. Avoid returning an external response body when it might echo request credentials or tokens. Prefer short-lived access tokens with the narrowest external scope and discard them when the outbound request completes.

GetRequired throws SecretUnavailableException for a valid but unavailable key. Branch on its Code, never its message:

StateSecretUnavailableCodes valueStable code
No key existsMissingmoltaro.secrets.missing
Key is disabledDisabledmoltaro.secrets.disabled
Key is retiredRetiredmoltaro.secrets.retired

TryGet returns false and sets its output to null for all three states. Both methods throw ArgumentException for null, empty, whitespace-padded, or null-character-containing keys because those are programming errors, not lifecycle states.

Missing usually means provisioning or spelling is wrong. Disabled and retired are deliberate administrative states. Do not loop or silently fall back to a credential from source. Let the invocation fail safely, have an owner or administrator resolve the configuration, and retry in a new invocation.

Moltaro loads one immutable managed-secret snapshot before constructing a function. Every lookup in that invocation uses the same snapshot and performs only synchronous in-memory work. Do not use .Result, .Wait(), or invent an asynchronous secret API.

An invocation already in progress may continue with the old value after a rotation or lifecycle change. A later invocation gets a fresh snapshot. This keeps one run internally consistent and makes the retry boundary explicit.

Managed-secret adoption is a handoff between people, an authoring agent, and the runtime. Keep those responsibilities separate:

ResponsibilityOwner
Choose the stable key names and enter or rotate valuesWorkspace owner or administrator
Reference the supplied key names and author trusted C#Developer or explicitly authorized coding agent
Load one immutable snapshot and resolve values in memoryMoltaro runtime
Approve rollout, external credential rotation, and schedulingThe integration’s operator

An agent can complete the C# integration without ever receiving a secret value:

  1. Start at public /llms.txt, then read the installation-generated AGENTS.md or CLAUDE.md supplied by the workspace owner.
  2. Fetch the installation’s Configuration OpenAPI document and Net Operation Project developer-surface. Confirm that Moltaro.Package.NET.Functions.ISecretService is in the supported service catalog.
  3. Ask the owner for the exact stable key names only. If a required key has not been provisioned, report that fact; do not ask for its value.
  4. Inject ISecretService into the function constructor. Prefer GetRequired for required integration configuration and use TryGet only when absence is an intended optional state.
  5. Create a source revision through the Configuration API, run Check, then run Build. Use the resulting published artifact for the intended Job, Command, Action, TriggerHandler, Validation, BeforeSaveMutation, or HttpEndpoint flow.
  6. Prove success with a non-secret result such as Matched, a boolean, or an ordinary marker field. Never echo a credential or derived token for testing.
  7. Ask the owner or administrator to inspect the key-local Audit Trail and the relevant function run or job before rollout.
  8. Hand provisioning, rotation, external-system changes, and schedule enablement back to the owner or integration operator. They are not implicit coding-agent actions.

An agent may enumerate, create, rotate, disable, enable, or retire secrets only when the assigned task explicitly authorizes that owner/admin operation. Never ask the user to paste a secret into a prompt, source file, function argument, or test assertion. Values must travel through an appropriate secret channel directly to Administration > Secrets, or to the write-only API when the task explicitly authorizes owner/admin automation. Keep keys stable across source revisions and make function output prove only a non-secret business outcome.

Continue with the AI agent development quickstart, the .NET SDK guide, and the installation’s Configuration API reference.