Skip to main content
Version: 13 - 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',
scheme='https',
host='api.example.com',
path='/things',
headers=[{'name': 'Authorization', 'value': 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 itself is held encrypted in the database and unlocked in memory by a passphrase that lives in the workspace environment, so a database dump or backup carries only ciphertext — the wrapped master key it contains is useless without that passphrase. 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 secret store is unlocked by a master key, and Cloudomation manages that key for you. On the first install the workspace mints a random master key, stores it encrypted (wrapped) in the database, and protects it with a passphrase held in the workspace environment as SECRET_STORE_PASSPHRASE. On startup the workspace reads the wrapped key from the database and unwraps it in memory with that passphrase; the cleartext master key is cached in process memory and is never written back. The installer auto-generates SECRET_STORE_PASSPHRASE on the first install and keeps it stable across deploys.

Keep the passphrase stable

SECRET_STORE_PASSPHRASE must stay the same for the life of the installation. Changing it orphans the wrapped master key in the database, and every secret already stored under that key becomes unrecoverable. Treat it like the database password: keep a backup, and restore the original value if you rebuild the environment.

A workspace administrator can also unlock the store interactively after a restart instead of keeping the passphrase in the environment: leave SECRET_STORE_PASSPHRASE unset and supply the passphrase through the unseal step. A passphrase that is present but wrong raises an error, so a misconfiguration is visible instead of silently leaving the store sealed.

As an alternative to the managed passphrase, the master key can be supplied directly as a raw url-safe base64, 32-byte Fernet key in SECRET_STORE_KEY. When that variable is set its key is used as-is, with no database wrapping.

When neither a passphrase-unlocked key nor a raw SECRET_STORE_KEY is available, 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 secret store is a workspace administration step, handled by the installer on a standard install. If you need to persist secrets and the store is sealed, ask your Cloudomation administrator to check the secret-store configuration (normally SECRET_STORE_PASSPHRASE).

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 passphrase that unlocks the master key lives in the host environment, so an attacker who can read that environment can unwrap the master key and 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.