Skip to main content
Version: 12 - TBD

Inline Secrets (system.secret)

system.secret(value) wraps a value as a secret: an opaque leaf value that masks and encrypts only itself wherever it is serialized — execution input/output, records, logs, git-sync, and database dumps. The value is stored as an encrypted envelope, never in cleartext, while your flow can still reveal the real value in memory when it needs to use it.

This complements the external secret managers (HashiCorp Vault, Devolutions Server). Those let you retrieve a secret from an external store; system.secret() lets you protect a value you already hold — for example a token you generated, received from an API, or assembled in a flow — so it does not leak into logs or persisted records.

Availability

system.secret() is available from Cloudomation Engine 12. Serializing or revealing a persisted secret requires the workspace secret store to be unsealed (a master key configured — see Configuring the secret store). Until then, a secret can be created and revealed in memory, but persisting one raises SecretStoreSealedError.

When to use it

Use system.secret() when a flow produces or handles a sensitive value that would otherwise be written out in cleartext:

  • A password, token, or key you generated or received at runtime and want to store in a record or return from a flow — encrypted, not in the clear.
  • Any field that should never appear in execution output, logs, or a git-synced record, but must still be usable later (decryptable on demand).

If the secret already lives in Vault or Devolutions, keep using the external secret manager to fetch it. Reach for system.secret() when you are the source of the value.

Wrapping and revealing a value

Call system.secret() with any plain JSON value. You get back a Secret wrapper:

def handler(system, this, inputs):
token = generate_api_token() # some sensitive value
secret = system.secret(token)

# Masked everywhere it could leak:
this.log(f'token is {secret}') # logs: token is *protected*
print(repr(secret)) # Secret(*protected*)

# Reveal the cleartext in memory when you actually need it:
real_token = secret.reveal()
this.connect(
'rest',
name='call downstream API',
url='https://api.example.com/things',
headers={'Authorization': f'Bearer {real_token}'},
)
  • str(secret) and repr(secret) render as *protected* / Secret(*protected*), so the value cannot leak through logging, f-strings, or error messages.
  • secret.reveal() returns the original cleartext. For a secret you just created in the flow this is an in-memory read; for a secret that was persisted and read back, reveal() decrypts it on demand (which requires the store to be unsealed).

Composing secrets into structures

A secret is a leaf: it composes into any JSON structure, and only the leaf itself is protected. Everything around it stays readable.

def handler(system, this, inputs):
this.save(output_value={
'stdout': 'command finished ok', # persisted readable
'password': system.secret(password), # persisted encrypted
})

When this output is serialized, stdout is stored as-is and password is replaced by an encrypted envelope. Reading the record back in a display context shows password masked, exactly like schema-based field masking. Reading it in a flow reconstructs a live Secret you can reveal().

Serialization and the secret store

Whenever a secret crosses a serialization boundary it is turned into an encrypted envelope instead of cleartext:

  • execution input_value / output_value
  • record fields (including custom-object values)
  • logs
  • git-synced resources
  • database dumps and backups

Encryption uses a per-secret random data key to encrypt the payload; that data key is then wrapped by the workspace master key. The master key lives outside the database (in the host environment), so a database dump or backup carries ciphertext only. Round-tripping a persisted secret through the database re-emits the same envelope rather than re-encrypting, so tracked records do not churn on every write.

Configuring the secret store

The master key is provided to the workspace via the SECRET_STORE_KEY environment variable (a url-safe base64, 32-byte Fernet key). When it is present the store is unsealed and secrets serialize and decrypt normally. When it is absent the store is sealed: creating and revealing an in-memory secret still works, but any attempt to serialize or decrypt one raises SecretStoreSealedError, for example:

command serialization error: secret store is sealed:
no master key (SECRET_STORE_KEY) is configured

Configuring the master key is a workspace administration step. Ask your Cloudomation administrator to set SECRET_STORE_KEY if you need to persist secrets and the store is sealed.

Threat model

Inline secrets defend against incidental leakage — secrets showing up in logs, git-synced records, and shared database dumps or backups — and remove the need for fragile masking workarounds. They are not a defense against a fully compromised host: the master key lives in the host environment, so an attacker who can read that environment can decrypt the ciphertext. Keep host access restricted accordingly.

Behavior reference

ExpressionResult
system.secret(value)A Secret wrapping value (must be plain JSON data)
str(secret)*protected*
repr(secret)Secret(*protected*)
secret.reveal()The cleartext value (decrypts on demand if persisted)
serialize a Secret leafEncrypted envelope (requires an unsealed store)
secret == otherIdentity comparison only (never compares cleartext)
hash(secret)Not hashable — secrets are intentionally not usable as dict keys or set members

Secret is deliberately neither value-comparable nor hashable: comparing or hashing by value would either leak the cleartext or require an unseal, so secrets use identity semantics instead.