{"record":{"id":"0e02b31b3dd74673","repo":"semaphoreui/semaphore","slug":"secret-does-not-belong-to-this-environment","errorCode":null,"errorMessage":"secret does not belong to this environment","messagePattern":"secret does not belong to this environment","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"api/projects/environment.go","lineNumber":93,"sourceCode":"\t\t\t\tSourceStorageID:   env.SecretStorageID,\n\t\t\t\tSourceStorageKey:  sourceStorageKey,\n\t\t\t\tSourceStorageType: storageType,\n\t\t\t})\n\n\t\t\tif err != nil {\n\t\t\t\terrors = append(errors, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\tcase db.EnvironmentSecretDelete:\n\t\t\tkey, err = c.accessKeyRepo.GetAccessKey(env.ProjectID, secret.ID)\n\n\t\t\tif err != nil {\n\t\t\t\terrors = append(errors, err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif key.EnvironmentID == nil || *key.EnvironmentID != env.ID {\n\t\t\t\terrors = append(errors, fmt.Errorf(\"secret does not belong to this environment\"))\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\terr = c.accessKeyService.Delete(env.ProjectID, secret.ID)\n\t\t\tif err != nil {\n\t\t\t\terrors = append(errors, err)\n\t\t\t\tcontinue\n\t\t\t}\n\t\tcase db.EnvironmentSecretUpdate:\n\t\t\tkey, err = c.accessKeyRepo.GetAccessKey(env.ProjectID, secret.ID)\n\n\t\t\tif err != nil {\n\t\t\t\terrors = append(errors, err)\n\t\t\t\tcontinue\n\t\t\t}\n\n\t\t\tif key.EnvironmentID == nil || *key.EnvironmentID != env.ID {\n\t\t\t\terrors = append(errors, fmt.Errorf(\"secret does not belong to this environment\"))","sourceCodeStart":75,"sourceCodeEnd":111,"githubUrl":"https://github.com/semaphoreui/semaphore/blob/1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa/api/projects/environment.go#L75-L111","documentation":"In updateEnvironmentSecrets (api/projects/environment.go:93), during the DELETE branch, each requested secret key is loaded and checked: if key.EnvironmentID is nil or differs from the environment being updated, the function appends 'secret does not belong to this environment' and skips deletion. This guards against removing a secret key through an environment that doesn't own it.","triggerScenarios":"Calling UpdateEnvironment or AddEnvironment with a secrets delete list containing a key whose AccessKey record has EnvironmentID nil or pointing to a different environment in the same project — e.g. deleting a key that was created under environment 'production' while updating environment 'staging'.","commonSituations":"Merged/pasted secret name lists across environments; API clients caching key IDs from another environment; keys created before environment scoping existed (nil EnvironmentID legacy rows); test TestUpdateEnvironmentSecrets_DeleteRejectsKeyFromOtherEnvironment exercises exactly this.","solutions":["Remove that key from the delete list for this environment call, or issue the delete against the environment that owns the key.","Query the secret's EnvironmentID (project access keys) and confirm it matches the target env.ID before requesting deletion.","For legacy keys with nil EnvironmentID, update the row to set the correct environment, then retry.","Verify via the API which environment each secret key belongs to before batch operations."],"exampleFix":"// before: deleting a key from the wrong env\n{\"secrets\": {\"delete\": [\"DB_PASSWORD\"]}}  // key owned by 'production'\n// after: only delete keys owned by this environment\n{\"secrets\": {\"delete\": [\"STAGING_DB_PASSWORD\"]}}","handlingStrategy":"validation","validationCode":"// verify ownership before requesting deletion\nconst key = await getAccessKey(env.projectId, keyName);\nif (key == null || key.environment_id !== env.id) {\n  throw new Error(`key ${keyName} is not owned by environment ${env.id}`);\n}","typeGuard":"function keyBelongsToEnv(key, env) {\n  return key != null && typeof key.environment_id === \"number\" &&\n    key.environment_id === env.id;\n}","tryCatchPattern":"try {\n  await updateEnvironment(env.id, { secrets: { delete: [keyName] } });\n} catch (e) {\n  if (/secret does not belong to this environment/.test(e.message)) {\n    // retry against the owning environment\n  }\n}","preventionTips":["Always scope secret operations to the environment that created the key","Fetch fresh key metadata instead of caching key ids across environments","Audit legacy rows with NULL environment_id and assign them","Avoid pasting secret lists between environment payloads"],"tags":["secrets","environment","validation"],"backgroundTag":"permission-denied","analyzedSha":"1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa","analyzedAt":"2026-09-07T11:00:33.293Z","contentChangedAt":"2026-09-07T11:00:33.293Z","schemaVersion":2},"datasetVersion":"2026-09-16T09:17:16.951Z"}