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',
scheme='https',
host='api.example.com',
path='/things',
headers=[{'name': 'Authorization', 'value': 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 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.
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).
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
| 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
- Protecting Child Outputs (secret_outputs) — declare which leaves of a child execution's output to protect from the parent flow.
- Secret Handling with Vault and Devolutions — retrieve secrets from an external secret manager.