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.
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)andrepr(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.
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
| Expression | Result |
|---|---|
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 leaf | Encrypted envelope (requires an unsealed store) |
secret == other | Identity 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.
Related
- Secret Handling with Vault and Devolutions — retrieve secrets from an external secret manager.