{"record":{"id":"fc28645d4d7749ce","repo":"iflytek/astron-agent","slug":"tenant-bootstrap-api-key-conflicts-with-an-unmanaged","errorCode":null,"errorMessage":"tenant bootstrap API key conflicts with an unmanaged credential","messagePattern":"tenant bootstrap API key conflicts with an unmanaged credential","errorType":"console","errorClass":null,"httpStatus":null,"severity":"error","filePath":"core/tenant/tools/database/bootstrap_credentials.go","lineNumber":196,"sourceCode":"\t\treturn false, fmt.Errorf(\"check tenant bootstrap API key ownership failed: %w\", err)\n\t}\n\n\tvar unmanagedSecret sql.NullString\n\tvar unmanagedIsDelete sql.NullBool\n\terr = transaction.QueryRowContext(\n\t\tctx,\n\t\t`SELECT api_secret, is_delete\n\tFROM tb_auth\n\tWHERE app_id = ? AND api_key = ? AND COALESCE(extend, '') <> ?\n\tLIMIT 1 FOR UPDATE`,\n\t\tcredentials.TenantID,\n\t\tcredentials.APIKey,\n\t\ttenantBootstrapManagedMarker,\n\t).Scan(&unmanagedSecret, &unmanagedIsDelete)\n\tif err == nil {\n\t\tif !unmanagedSecret.Valid || !unmanagedIsDelete.Valid || unmanagedIsDelete.Bool ||\n\t\t\tsubtle.ConstantTimeCompare([]byte(unmanagedSecret.String), []byte(credentials.Secret)) != 1 {\n\t\t\treturn false, errors.New(\"tenant bootstrap API key conflicts with an unmanaged credential\")\n\t\t}\n\t\treturn true, nil\n\t}\n\tif err != nil && !errors.Is(err, sql.ErrNoRows) {\n\t\treturn false, fmt.Errorf(\"check tenant bootstrap managed credential failed: %w\", err)\n\t}\n\treturn false, nil\n}\n\nfunc adoptTenantBootstrapCredential(\n\tctx context.Context,\n\ttransaction bootstrapTransaction,\n\tcredentials config.TenantBootstrapCredentials,\n\tnow string,\n) error {\n\t// A strong pair explicitly configured by the deployment may already have\n\t// been created through Tenant's public API on an older release. Because it\n\t// belongs to the reserved app and exactly matches the current deployment","sourceCodeStart":178,"sourceCodeEnd":214,"githubUrl":"https://github.com/iflytek/astron-agent/blob/5e758547a83371a5a4b29dadf4ac03e8dd527635/core/tenant/tools/database/bootstrap_credentials.go#L178-L214","documentation":"When the bootstrap api_key row is not managed (missing the managed marker), it is treated as an unmanaged credential. The function tolerates it only if the row is valid, not deleted, and its secret matches the configured secret via constant-time compare; otherwise it reports a conflict between the bootstrap API key and an unmanaged credential.","triggerScenarios":"A tb_app row with the same api_key exists but was created outside the bootstrap flow (no tenantBootstrapManagedMarker), and either its secret differs from credentials.Secret, its secret/flags are NULL, or the row is soft-deleted.","commonSituations":"The api_key was issued manually or by a legacy provisioning tool, credentials were rotated in config but not in the DB, or the managed marker was lost after a manual DB edit.","solutions":["Align the configured bootstrap secret with the existing row's secret (or vice versa) so they match","Migrate the existing row into the managed flow by adding the managed marker and correct secret","Choose a new api_key value with no pre-existing row","Clean up the soft-deleted/NULL-flagged conflicting row before re-running bootstrap"],"exampleFix":"// before\nconf = {APIKey: \"ak-1\", Secret: \"new-secret\"} // DB row has old-secret, unmanaged\n// after\nconf = {APIKey: \"ak-1\", Secret: \"old-secret\"} // matches unmanaged row, or re-key","handlingStrategy":"validation","validationCode":"var secret string; var marker string\nerr := db.QueryRow(`SELECT secret, marker FROM tb_app WHERE api_key = ?`, apiKey).Scan(&secret, &marker)\nif err == nil && marker != managedMarker && secret != configuredSecret { return errors.New(\"api_key conflicts with unmanaged credential\") }","typeGuard":null,"tryCatchPattern":"if err != nil && strings.Contains(err.Error(), \"unmanaged credential\") { rotateBootstrapKey(ctx, creds); return reconcileBootstrap(ctx, creds) }","preventionTips":["Provision apps only through the managed bootstrap flow","Rotate bootstrap credentials in DB and config atomically","Preserve the managed marker column in migrations"],"tags":["go","mysql","bootstrap","credential-conflict"],"backgroundTag":"conflicting-config-options","analyzedSha":"5e758547a83371a5a4b29dadf4ac03e8dd527635","analyzedAt":"2026-09-12T08:03:51.356Z","contentChangedAt":"2026-09-12T08:03:51.356Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}