Secret-Attribute in der API
Diese Seite beschreibt, wie die GraphQL-API des Asset Repository, die REST-API der Identity Provider, MCP und octo-cli Attribute vom Werttyp Secret behandeln. Die Regel in einem Satz: Sie können ein Secret schreiben und sehen, ob es gesetzt ist — keine API gibt den Wert zurück.
Verfügbar ab dem System-CK-Modell 2.5.0 und dem Engine- und Asset-Repository-Release, das den Werttyp Secret ausliefert. Die CK-Modell-API meldet den Werttyp als SECRET.
Lesen
Typisierte Felder
Ein Secret-Attribut wird als Feld vom Typ OctoSecretState statt String bereitgestellt:
type OctoSecretState {
isSet: Boolean! # false, wenn nicht gesetzt ODER wenn der gespeicherte Wert nicht lesbar ist
keyMissing: Boolean! # true: ein Wert ist gespeichert, aber seine Key-ID fehlt im Key Ring dieser Umgebung
setAt: DateTime # wann der aktuelle Wert gesetzt wurde (UTC); null, wenn nicht gesetzt oder vor diesem Feature gesetzt
}
| Attribut | Feldtyp | Ergebnis, wenn nicht gesetzt |
|---|---|---|
| Pflicht-Secret-Attribut | OctoSecretState! | { "isSet": false, "keyMissing": false, "setAt": null } |
| Optionales Secret-Attribut | OctoSecretState | Dasselbe Objekt — das Feld ist im Schema nullable, liefert aber immer ein Objekt |
keyMissing: true (zusammen mit isSet: false) kennzeichnet einen Wert, der mit einem Schlüssel verschlüsselt gespeichert ist, den die Umgebung nicht hat — typischerweise nach einem Restore aus einer anderen Umgebung. Siehe Nicht lesbare Secrets.
query {
runtime {
systemCommunicationEMailSenderConfiguration(rtId: "65f1c0a2b3d4e5f601234567") {
items {
rtId
name
userName
password {
isSet
keyMissing
setAt
}
}
}
}
}
{
"rtId": "65f1c0a2b3d4e5f601234567",
"name": "Notifications",
"userName": "service@example.com",
"password": { "isSet": true, "keyMissing": false, "setAt": "2026-10-06T08:15:00Z" }
}
Bestehende Dokumente, die ein Secret-Feld als Skalar auswählen (password ohne Sub-Selektion), scheitern an der GraphQL-Validierung. Das ist beabsichtigt: Ein Client, der einen String erwartet, scheitert laut, statt einen Wert zu erhalten.
Generische Projektion
Die generische Attribut-Projektion liefert für ein Secret-Attribut value: null und meldet den Zustand in secretIsSet, secretKeyMissing und secretSetAt. Alle drei sind für jedes Attribut, das kein Secret ist, null.
type RtEntityAttribute {
attributeName: String
value: SimpleScalar # für ein Secret-Attribut immer null
secretIsSet: Boolean # null für Nicht-Secret-Attribute, true/false für Secrets
secretKeyMissing: Boolean # null für Nicht-Secret-Attribute; true, wenn gespeichert, aber nicht lesbar (unbekannte Key-ID)
secretSetAt: DateTime # null für Nicht-Secret-Attribute, Legacy-Werte und nicht gesetzte Secrets
}
query {
runtime {
runtimeEntities(ckId: "System.Communication/EMailSenderConfiguration", rtId: "65f1c0a2b3d4e5f601234567") {
items {
attributes {
items {
attributeName
value
secretIsSet
}
}
}
}
}
}
{ "attributeName": "password", "value": null, "secretIsSet": true }
Eine solche Projektion in einem Update zurückzusenden ist sicher — sie bedeutet „unverändert“ (siehe unten).
Schreiben
Der Eingabetyp eines Secret-Attributs bleibt String (typisierte Inputs: password: String; Record-Member: token: String). Jede nicht leere Eingabe wird vor dem Speichern mit dem aktiven Schlüssel verschlüsselt; ein verschlüsselter Wert kann daher über die API nie von einer Entität auf eine andere kopiert werden.
| Eingabe | Wirkung |
|---|---|
| Nicht leerer String | Setzt oder rotiert das Secret (der Server verschlüsselt es) |
Feld weggelassen, null, "" | Unverändert |
Zurückgesendetes Lese-Objekt ({ isSet }) oder secretIsSet im generischen Input | Unverändert |
Name in clearSecretAttributes | Gelöscht (Fehler, wenn das Secret Pflicht ist) |
In Records: Member weggelassen, null oder "" | Aus dem gespeicherten Element mit demselben Record-Key übernommen |
Der generische Input RtEntityAttributeInput { attributeName, value, secretIsSet } akzeptiert secretIsSet und ignoriert es; das Zurücksenden des Gelesenen ist daher sicher. Das Anlegen einer Entität erfordert einen Wert für jedes Pflicht-Secret-Attribut. Ein gespeichertes, aber nicht lesbares Secret (keyMissing) gilt für diese Prüfung als vorhanden.
Platzhalter haben keine Bedeutung: Ein String wie <client-secret> oder TODO_SET_CLIENT_SECRET, der über eine beliebige API gesendet wird, ist ein gewöhnlicher Wert und wird wie jeder andere verschlüsselt. Nur die einmalige Migration von Legacy-Klartext behandelt solche Strings besonders (siehe Sweep).
Ein Secret setzen oder rotieren
mutation {
runtime {
systemCommunicationEMailSenderConfigurations {
update(entities: [{ rtId: "65f1c0a2b3d4e5f601234567", item: { password: "example-password" } }]) {
rtId
password {
isSet
}
}
}
}
}
Andere Secrets der Entität bleiben unberührt. Das Ergebnis der Mutation folgt den Leseregeln: Es liefert { isSet }, nie den Wert.
Ein Secret löschen
Da null „unverändert“ bedeutet, wird explizit gelöscht. clearSecretAttributes: [String!] (Attributnamen in camelCase) steht am Update-Eintrag — neben rtId und item, nicht in item. Sowohl das typisierte <Type>InputUpdate als auch das generische RtEntityUpdate haben es:
mutation {
runtime {
systemCommunicationEMailSenderConfigurations {
update(entities: [{ rtId: "65f1c0a2b3d4e5f601234567", item: {}, clearSecretAttributes: ["password"] }]) {
rtId
password {
isSet
}
}
}
}
}
- Das Löschen eines Pflicht-Secret-Attributs ist ein Fehler.
- Dasselbe Secret in einem Update zu setzen und zu löschen ist ein Fehler.
MutationDto.ClearSecretAttributesim SDK trägt dieselbe Liste.
Abfragen
| Operation | Secret-Attribute |
|---|---|
| Feldfilter | Nur IS_NULL und IS_NOT_NULL („gesetzt“ = das Feld existiert und ist nicht null) |
| Sortierung | Abgelehnt |
| Attribut- (Text-)Suche | Abgelehnt |
| Aggregation, Group-by | Abgelehnt |
| Query-Spalten, Zellen von Persistent-Query-Zeilen | Abgelehnt |
| Archiv-Spaltenpfade, CrateDB-Spalten | Nicht verfügbar |
Entitäten mit konfiguriertem Secret zählen:
query {
runtime {
systemCommunicationEMailSenderConfiguration(
fieldFilter: [{ attributePath: "password", operator: IS_NOT_NULL }]
) {
totalCount
}
}
}
Fehlercodes
| Code | Wann | Details |
|---|---|---|
SecretAttributeNotQueryable | Ein anderer Filter als IS_NULL / IS_NOT_NULL, Sortierung, Suche, Aggregation, Group-by, Query-Spalte oder Query-Zeilen-Zelle auf einem Secret-Attribut | extensions.attributePath, extensions.operation |
ASSET1004 (Validierung) | Pflicht-Secret fehlt beim Anlegen (Meldung 2), Löschen eines Nicht-Secrets oder unbekannten Namens (21), Löschen eines Pflicht-Secrets (22), gleichzeitig gesetzt und gelöscht (23), Pflicht-Secret fehlt nach der Record-Übernahme (24), Nicht-String-Eingabe | Meldungsnummern in OctoDetails als "NN: Text" — nie Werte |
SecretEncryptionNotConfigured | Der Server hat keinen Key Ring | — |
Berechtigungen
Datenberechtigungen bleiben Zeilenfilter auf Typebene:
- Lesezugriff auf eine Entität zeigt, ob ihre Secrets gesetzt sind.
- Das Schreiben eines Secrets erfordert Schreibzugriff auf die Entität.
- Keine Berechtigung erlaubt das Entschlüsseln über die öffentliche API. Es gibt keinen Entschlüsselungs-Endpunkt.
Zwei Tenant-Rollen regeln die Secret-Verwaltung (der initiale Tenant-Administrator hat beide):
| Rolle | Erlaubt |
|---|---|
AdminPanelManagement | Secrets-Übersicht, Sweep-Reports und die Liste der Sweep-Läufe |
SecretManagement | Sweeps starten und einen Pre-Sweep-Dump vorzeitig löschen (siehe Sweep). Angelegt von System.Identity.Bootstrap; Konstante CommonConstants.SecretManagementRole |
Secrets werden nur in vertrauenswürdigen Services entschlüsselt: im Communication Controller, wenn er die Adapter-Konfiguration erstellt, und im Mesh Adapter, wenn eine Pipeline RevealSecret@1 ausführt. Jede Entschlüsselung wird gezählt (octo.secrets.decrypt); Logs enthalten nie den Wert.
Secrets in Records
Ein Record-Array wird immer als Ganzes geschrieben. Bei Update und Replace, in jeder Verschachtelungstiefe, wird ein Secret-Member, der in einem eingehenden Element weggelassen, null oder "" ist, aus dem gespeicherten Element mit demselben Record-Key übernommen (der recordKey des Record-Typs). Ein einzelnes Record-Attribut übernimmt über die Position. Keys werden über den Wert verglichen (Ganzzahlen über numerische Typen hinweg, sonst ordinaler Text). Ein Record-Array ohne Key (Modelle von vor der recordKey-Regel) erhält keine Übernahme.
Beispiel: Der gespeicherte Wert hat zwei Elemente mit den Keys smtp und imap; das Update sendet:
[
{ "path": "smtp", "secretValue": null },
{ "path": "imap", "secretValue": "example-password-1" },
{ "path": "pop3", "secretValue": "example-password-2" }
]
Ergebnis: smtp behält sein gespeichertes Secret, imap erhält das neue Secret, pop3 wird hinzugefügt. Ein Element, das nicht mehr gesendet wird, wird mitsamt seinem Secret entfernt. Ein Secret-Member in einem Record kann nicht einzeln gelöscht werden — clearSecretAttributes benennt nur Attribute der obersten Ebene; lassen Sie das Element weg, um es zu entfernen, oder senden Sie einen neuen Wert, um es zu ersetzen.
In der Projektion zeigt der Secret-Member eines Record-Elements { isSet } wie ein typisiertes Feld (endpoints { key label token { isSet } }).
Identity Provider (REST)
Der Identity Service speichert das ClientSecret der OAuth-Provider als Secret-Attribut (System.Identity 2.23). Für GoogleIdentityProviderDto, MicrosoftIdentityProviderDto, FacebookIdentityProviderDto und AzureEntraIdProviderDto auf {tenantId}/v1/identityProviders gilt:
| Operation | clientSecret |
|---|---|
Antworten auf GET (Liste und per ID), POST und PUT | Immer null (nur schreibbar). Die PUT-Antwort wird aus der gespeicherten Entität erstellt, nicht zurückgespiegelt |
POST | Pflicht, nicht leer |
PUT | Nicht leer rotiert; null, "" oder weggelassen behalten das gespeicherte Secret. Es kann nicht gelöscht werden (das Secret ist Pflicht) |
Ein Wert, der wie ein Platzhalter aussieht (<…>, TODO_SET_*), ist ein gewöhnliches Client Secret. Die Antworten tragen statt des Werts den Zustand (jedes Feld entfällt bei null):
| Feld | Bedeutung |
|---|---|
clientSecretIsSet: boolean | Ein lesbares Client Secret ist gespeichert |
clientSecretKeyMissing: boolean | Ein Client Secret ist gespeichert, aber seine Key-ID fehlt im Key Ring (clientSecretIsSet ist dann false). Der Provider wird bei der Anmeldung mit einem Fehler-Log übersprungen, bis das Secret erneut eingegeben wird |
clientSecretSetAt: string (ISO-8601) | Wann das aktuelle Client Secret gesetzt wurde; null für Legacy-Werte |
octo-cli -c UpdateIdentityProvider und das MCP-Tool update_identity_provider senden ein Client Secret nur, wenn ein neues angegeben wird (siehe Identity Provider).
Nicht lesbare Secrets (Key Missing)
Ein geschützter Wert, dessen Key-ID nicht im Key Ring der Umgebung ist — typischerweise nach dem Restore eines Dumps aus einer anderen Umgebung oder eines Child-Tenants — bleibt verschlüsselt gespeichert:
- Leser sehen
isSet: falseundkeyMissing: true(generisch:secretIsSet: false,secretKeyMissing: true); die Secrets-Übersicht meldet die FormKEY_MISSINGund führt den Wert als Neueingabe-Aufgabe. - Services, die den Wert brauchen (Adapter-Konfiguration,
RevealSecret@1, Identity Provider, AI-Services), behandeln ihn als nicht gesetzt und loggen einen Fehler ohne den Wert. - Er wird automatisch wieder lesbar, sobald seine Key-ID dem Key Ring hinzugefügt wird.
- Er wird nur durch die Eingabe eines neuen Werts (Neueingabe), durch Löschen mit
clearSecretAttributes(optionale Secrets) oder durch den Admin-SweepCleanupUnreadableentfernt. Keine API entschlüsselt oder exportiert Klartext.
Ein gespeicherter Wert, der sich gar nicht parsen lässt, gilt ebenfalls als nicht gesetzt (Form CORRUPT, Warnung im Log).
Secrets-Übersicht (Admin-GraphQL)
Das Asset Repository bietet eine Tenant-weite Übersicht über alle Secret-Attribute, einschließlich der Client Secrets der Identity Provider (System.Identity/*) und der AI-Zugangsdaten (System.Ai/*). Sie erfordert die Tenant-Rolle AdminPanelManagement (sonst Fehlercode Forbidden) und liefert nie Werte. Es gibt keine Aggregation über Child-Tenants: Fragen Sie den Endpunkt jedes Child-Tenants ab.
type Query { secrets: SecretsQuery }
type SecretsQuery {
inventory(
first: Int = 50, after: String,
ckTypeId: String, # exakter CK-Typ (abgeleitete Typen eingeschlossen)
forms: [SecretStorageForm!],
needsReEntry: Boolean, # nur Neueingabe-Aufgaben
search: String # rtId, rtWellKnownName, Anzeigename, Attributpfad (nie Werte)
): SecretInventoryConnection!
summary: SecretInventorySummary!
usages(ckTypeId: String!, rtId: OctoObjectId!, attributePath: String!): [SecretUsage!]!
}
enum SecretStorageForm { NOT_SET, PLAINTEXT, ENC_V1, ENC_V2, KEY_MISSING, CORRUPT }
type SecretInventoryItem {
ckTypeId: String!
rtId: OctoObjectId!
rtWellKnownName: String
displayName: String
attributePath: String! # camelCase; Record-Member als "endpoints[key=prod].token" / "credentials.token"
attributeName: String! # CK-Attributname des Attributs der obersten Ebene
required: Boolean!
form: SecretStorageForm!
keyId: String # nur ENC_V2 / KEY_MISSING
setAt: DateTime
needsReEntry: Boolean! # KEY_MISSING oder CORRUPT, oder NOT_SET und Pflicht
usedBy: [SecretUsage!]!
}
type SecretUsage {
dataFlowRtId: OctoObjectId
dataFlowName: String
pipelineRtId: OctoObjectId!
pipelineName: String
nodePath: String!
match: SecretUsageMatch! # EXACT: RevealSecret@1 mit ckTypeId + rtId + attributeName; BY_TYPE: rtIdPath auf demselben Typ und Attribut
}
type SecretInventorySummary {
total: Int!, notSet: Int!, plaintext: Int!, encV1: Int!, encV2: Int!, keyMissing: Int!, corrupt: Int!, needsReEntry: Int!
encV2ByKeyId: [KeyIdCount!]!
}
| Form | Bedeutung |
|---|---|
NOT_SET | Kein Wert |
PLAINTEXT | Legacy-Klartext noch gespeichert (vor dem Encrypt-Sweep) |
ENC_V1 | Legacy-Wert enc:v1 (Instance Key) |
ENC_V2 | Geschützt, Key-ID bekannt |
KEY_MISSING | Geschützt, Key-ID nicht im Key Ring dieser Umgebung |
CORRUPT | Gespeicherter Wert lässt sich nicht parsen (gilt als nicht gesetzt) |
Neueingabe-Aufgaben sind live: inventory(needsReEntry: true). Eine Aufgabe ist erledigt, wenn ein neuer Wert gesetzt wurde (→ ENC_V2), das optionale Secret gelöscht wurde oder der Sweep CleanupUnreadable es entfernt hat.
query {
secrets {
summary { total keyMissing needsReEntry encV2ByKeyId { keyId count } }
inventory(needsReEntry: true, first: 20) {
totalCount
items { ckTypeId rtId displayName attributePath form keyId setAt }
}
}
}
Exporte, Backups, MCP und octo-cli
ExportRtund Deep-Graph-Exporte enthalten nie Secret-Werte (OwnershipSecret).- Backups (Dumps) enthalten die verschlüsselten Werte; ein Restore behält sie verschlüsselt (siehe Nicht lesbare Secrets und Restore über Umgebungen hinweg).
- Debug-Snapshots von Pipelines maskieren offengelegte Werte als
***und listen die maskierten Stellen inredactedPaths(sieheRevealSecret@1). - MCP (Tool-Referenz): Jedes Tool, das Entitäten liefert, zeigt
value: nullplussecretIsSet.create_entityundupdate_entity(mittleres Risiko) lehnen einen nicht leeren Secret-Wert ab;null,""oder ein zurückgesendetes{ "isSet": … }lassen das Secret unverändert, undupdate_entityakzeptiertclearSecretAttributes. Ein Secret wird mit dem Toolset_entity_secrets(hohes Risiko) gesetzt oder rotiert; eine Entität, deren Typ ein Pflicht-Secret hat, wird mit dem Toolcreate_entity_with_secrets(hohes Risiko) angelegt. Abfrage- und Aggregations-Tools lehnen Secret-Attribute mitSecretAttributeNotQueryableab.get_secret_statusundstart_secret_sweepdecken den Verschlüsselungsstatus und den Sweep ab. - octo-cli:
SecretStatus,ReprotectSecretsundDeleteSecretSweepDump(siehe Secret Encryption Key Ring).
Siehe auch
- Secret-Attribute — der Werttyp und seine Compiler-Regeln
- Update — allgemeine Update-Semantik
- Secret Encryption Key Ring — Betrieb