semaphoreui/semaphore · error

secret does not belong to this environment

Error message

secret does not belong to this environment

What it means

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.

Solutions

  1. Remove that key from the delete list for this environment call, or issue the delete against the environment that owns the key.
  2. Query the secret's EnvironmentID (project access keys) and confirm it matches the target env.ID before requesting deletion.
  3. For legacy keys with nil EnvironmentID, update the row to set the correct environment, then retry.
  4. Verify via the API which environment each secret key belongs to before batch operations.

Example fix

// before: deleting a key from the wrong env
{"secrets": {"delete": ["DB_PASSWORD"]}}  // key owned by 'production'
// after: only delete keys owned by this environment
{"secrets": {"delete": ["STAGING_DB_PASSWORD"]}}
Defensive patterns

Strategy: validation

Validate before calling

// verify ownership before requesting deletion
const key = await getAccessKey(env.projectId, keyName);
if (key == null || key.environment_id !== env.id) {
  throw new Error(`key ${keyName} is not owned by environment ${env.id}`);
}

Type guard

function keyBelongsToEnv(key, env) {
  return key != null && typeof key.environment_id === "number" &&
    key.environment_id === env.id;
}

Try / catch

try {
  await updateEnvironment(env.id, { secrets: { delete: [keyName] } });
} catch (e) {
  if (/secret does not belong to this environment/.test(e.message)) {
    // retry against the owning environment
  }
}

Prevention

When it happens

Trigger: 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'.

Common situations: 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.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07). Data as JSON: /api/errors/0e02b31b3dd74673. Report an issue: GitHub.

Appendix: source

Thrown at api/projects/environment.go:93

				SourceStorageID:   env.SecretStorageID,
				SourceStorageKey:  sourceStorageKey,
				SourceStorageType: storageType,
			})

			if err != nil {
				errors = append(errors, err)
				continue
			}
		case db.EnvironmentSecretDelete:
			key, err = c.accessKeyRepo.GetAccessKey(env.ProjectID, secret.ID)

			if err != nil {
				errors = append(errors, err)
				continue
			}

			if key.EnvironmentID == nil || *key.EnvironmentID != env.ID {
				errors = append(errors, fmt.Errorf("secret does not belong to this environment"))
				continue
			}

			err = c.accessKeyService.Delete(env.ProjectID, secret.ID)
			if err != nil {
				errors = append(errors, err)
				continue
			}
		case db.EnvironmentSecretUpdate:
			key, err = c.accessKeyRepo.GetAccessKey(env.ProjectID, secret.ID)

			if err != nil {
				errors = append(errors, err)
				continue
			}

			if key.EnvironmentID == nil || *key.EnvironmentID != env.ID {
				errors = append(errors, fmt.Errorf("secret does not belong to this environment"))

View on GitHub (pinned to 1774ccb71a)