{"record":{"id":"7e38d0158b68aa0e","repo":"semaphoreui/semaphore","slug":"cannot-modify-secret-in-read-only-storage","errorCode":null,"errorMessage":"cannot modify secret in read-only storage","messagePattern":"cannot modify secret in read-only storage","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"services/server/access_key_encryption_svc.go","lineNumber":18,"sourceCode":"package server\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/semaphoreui/semaphore/db\"\n\t\"github.com/semaphoreui/semaphore/pkg/common_errors\"\n\t\"github.com/semaphoreui/semaphore/pkg/tz\"\n\tpro \"github.com/semaphoreui/semaphore/pro/services/server\"\n)\n\nconst RekeyBatchSize = 100\n\nvar ErrReadOnlyStorage = errors.New(\"cannot modify secret in read-only storage\")\n\n// ErrAccessKeyExpired is returned when a key with ExpireAt in the past is\n// deserialized. Expired secrets must never be usable.\nvar ErrAccessKeyExpired = errors.New(\"access key expired\")\n\ntype AccessKeyEncryptionService interface {\n\tSerializeSecret(key *db.AccessKey) error\n\tDeserializeSecret(key *db.AccessKey) error\n\tFillEnvironmentSecrets(env *db.Environment, deserializeSecret bool) error\n\tDeleteSecret(key *db.AccessKey) error\n\tRekeyAccessKeys(oldKey string) (err error)\n\n\t// Task survey secrets: task-bound, expiring access keys\n\t// (owner AccessKeyTaskSecret). See task_secret_svc.go.\n\tCreateTaskSurveySecrets(projectID int, taskID int, secrets string, expireAt time.Time) error\n\tGetTaskSurveySecrets(projectID int, taskID int) (string, error)\n\tDeleteTaskSurveySecrets(projectID int, taskID int) error\n}","sourceCodeStart":1,"sourceCodeEnd":36,"githubUrl":"https://github.com/semaphoreui/semaphore/blob/1774ccb71a0a8b82eb74ea24c23ac9ab713de2fa/services/server/access_key_encryption_svc.go#L1-L36","documentation":"ErrReadOnlyStorage is a sentinel error indicating a secret write (serialize/create/rekey) was attempted while the access-key encryption service is in read-only mode (e.g. a remote/vault storage that does not permit modification, or a read-only operational mode). Callers should test with errors.Is; writes to existing secret material are simply not permitted in this mode.","triggerScenarios":"SerializeSecret (via getDeserializer with readonly=true) or Create on the encryption service when the resolved secret storage is read-only; the service wraps it with common_errors.NewUserError(ErrReadOnlyStorage).","commonSituations":"Pointing a project's secret storage at a vault/storage configured read-only; creating access keys while the backend storage is in migration or replica mode; tests exercising read-only behavior.","solutions":["Switch the project's secret storage to a writable storage backend (or flip the storage's read-only flag).","Use errors.Is(err, ErrReadOnlyStorage) to detect this case and skip/park write operations instead of retrying.","If storage should be writable, fix the storage configuration (credentials, mode) and retry the operation."],"exampleFix":"// before\nerr := svc.SerializeSecret(&key) // fails with read-only storage\n// after\nif err := svc.SerializeSecret(&key); errors.Is(err, server.ErrReadOnlyStorage) {\n    log.Warn(\"storage read-only; skipping secret write\")\n    return nil\n}","handlingStrategy":"try-catch","validationCode":"if storage.ReadOnly {\n    return fmt.Errorf(\"storage %d is read-only; cannot write secrets\", storage.ID)\n}","typeGuard":"func writable(s SecretStorage) bool { return !s.ReadOnly }","tryCatchPattern":"if err := svc.SerializeSecret(&key); err != nil {\n    if errors.Is(err, server.ErrReadOnlyStorage) {\n        log.Warn(\"read-only storage; secret not written\")\n        return nil\n    }\n    return err\n}","preventionTips":["Check the storage backend's read-only mode before enabling it for a project.","Use errors.Is against ErrReadOnlyStorage rather than string matching.","Surface read-only status in the UI before users attempt secret writes."],"tags":["secrets","access-key","read-only","storage"],"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"}