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.
| Field | What it is |
|---|---|
hint | What the value is allowed to say about itself |
hash | A 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.
| Value | Hint |
|---|---|
sk-live-9f2a7c4e18b6d05 | sk-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 holds | Sees |
|---|---|
VariableRead only | value is empty; sensitive, hint and hash are present |
VariableRead + VariableReadSecrets | The 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
| Endpoint | Behaviour without VariableReadSecrets |
|---|---|
GET /variables | Sensitive 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 /variablevalues | Sensitive values replaced by their hint before resolution |
GET /variablevalues/preview | Winning 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.
{
"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.