Skip to main content

Sensitive values

Any value can be marked sensitive. Doing so changes three things: what is stored alongside it, who is shown it, and what they are shown instead.

What is stored

Marking a value sensitive derives two extra fields, and they are written in exactly one place — SensitiveValueRules.Apply — so a value can never carry a hint for something it no longer is.

FieldWhat it is
hintWhat the value is allowed to say about itself
hashA SHA-256 of the value, salted with the organization ID

A value that stops being sensitive keeps neither: both are cleared.

The hint

The hint exists so that whoever set a secret can tell which one it is without being shown it.

  • Longer than 15 characters (ignoring whitespace) — the first 4 characters.
  • 15 characters or fewer — a run of asterisks, one per non-whitespace character.

Whitespace is neither counted nor shown. A value padded with spaces is not treated as the long value it is not, and a hint never opens with characters that say nothing.

ValueHint
sk-live-9f2a7c4e18b6d05sk-l
hunter2*******
short *****

The hash

The hash lets a value be recognised without being read — telling whether two variables hold the same secret, or whether an incoming value is actually a change. It is salted with the organization ID, so the same secret held by two organizations does not hash to the same thing.

It is computed over the value exactly as given, whitespace included: two values differing only by a space are two different secrets, whatever their hints have in common.

Who is shown what

One permission decides it, and one helper — CanReadSecrets — asks the question, so a secret is withheld by the same rule wherever it is read from.

VariableReadSecrets — which itself depends on VariableRead, because reading a secret says nothing without being able to read the variable holding it.

Caller holdsSees
VariableRead onlyvalue is empty; sensitive, hint and hash are present
VariableRead + VariableReadSecretsThe value itself

The hint and hash go out either way. They are what a withheld value has instead of itself, and without them a caller could not tell which secret they were being refused.

Every endpoint that can return a value

EndpointBehaviour without VariableReadSecrets
GET /variablesSensitive values blank
GET /variables/{id}Sensitive values blank
PUT /variables/{id}The response reflecting the write has them blank too
GET /variablesets/{id}Sensitive values blank throughout the set
PUT /variablesets/{id}As above
GET /variablevaluesSensitive values replaced by their hint before resolution
GET /variablevalues/previewWinning value returned with an empty value and its hint

The difference between the last two is worth restating. /variablevalues substitutes the hint into the value before resolving, so whatever wins is already the hint — the consumer gets a usable-looking map in which the sensitive entries are hints. /variablevalues/preview resolves first and blanks on the way out, so a withheld secret is visibly absent rather than quietly wrong.

Writing secrets

Writing is a different question from reading, and needs no secret permission at all. VariableCreate and VariableUpdate are enough to set a sensitive value.

The same asymmetry applies to import: BatchProcessingImport covers writing values, sensitive ones included, and VariableReadSecrets is deliberately absent from its dependencies. An import writes secrets and is never shown one back.

POST /variables — a sensitive value
{
"name": "ApiToken",
"variableSetId": 1,
"values": [
{ "value": "sk-live-9f2a7c4e18b6d05", "sensitive": true, "tags": ["Environment/Production"] }
]
}

What this is not

Values are stored in the database as given. Marking one sensitive controls who is shown it and what they see instead — it is not encryption at rest, and it does not stop anyone with database access from reading it.

The same caveat applies to identity provider client secrets held on an organization record: they are returned by GET /organizations/{id} to holders of OrganizationRead, and by the anonymous GET /organizations/{id}/signin. Encrypting those at rest is a known outstanding item, and their present protection is that the account API is not reachable from outside the cluster.