semaphoreui/semaphore · error
cannot modify secret in read-only storage
Error message
cannot modify secret in read-only storage
What it means
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.
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.
Example fix
// before
err := svc.SerializeSecret(&key) // fails with read-only storage
// after
if err := svc.SerializeSecret(&key); errors.Is(err, server.ErrReadOnlyStorage) {
log.Warn("storage read-only; skipping secret write")
return nil
} Defensive patterns
Strategy: try-catch
Validate before calling
if storage.ReadOnly {
return fmt.Errorf("storage %d is read-only; cannot write secrets", storage.ID)
} Type guard
func writable(s SecretStorage) bool { return !s.ReadOnly } Try / catch
if err := svc.SerializeSecret(&key); err != nil {
if errors.Is(err, server.ErrReadOnlyStorage) {
log.Warn("read-only storage; secret not written")
return nil
}
return err
} Prevention
- 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.
When it happens
Trigger: 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).
Common situations: 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.
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
- unsupported secret storage type
- access key expired
- vault storage id is required
- secret does not belong to this environment
- name can not be empty
AI-assisted analysis of semaphoreui/semaphore@1774ccb71a (2026-09-07).
Data as JSON: /api/errors/7e38d0158b68aa0e.
Report an issue: GitHub.
Appendix: source
Thrown at services/server/access_key_encryption_svc.go:18
package server
import (
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/semaphoreui/semaphore/db"
"github.com/semaphoreui/semaphore/pkg/common_errors"
"github.com/semaphoreui/semaphore/pkg/tz"
pro "github.com/semaphoreui/semaphore/pro/services/server"
)
const RekeyBatchSize = 100
var ErrReadOnlyStorage = errors.New("cannot modify secret in read-only storage")
// ErrAccessKeyExpired is returned when a key with ExpireAt in the past is
// deserialized. Expired secrets must never be usable.
var ErrAccessKeyExpired = errors.New("access key expired")
type AccessKeyEncryptionService interface {
SerializeSecret(key *db.AccessKey) error
DeserializeSecret(key *db.AccessKey) error
FillEnvironmentSecrets(env *db.Environment, deserializeSecret bool) error
DeleteSecret(key *db.AccessKey) error
RekeyAccessKeys(oldKey string) (err error)
// Task survey secrets: task-bound, expiring access keys
// (owner AccessKeyTaskSecret). See task_secret_svc.go.
CreateTaskSurveySecrets(projectID int, taskID int, secrets string, expireAt time.Time) error
GetTaskSurveySecrets(projectID int, taskID int) (string, error)
DeleteTaskSurveySecrets(projectID int, taskID int) error
}View on GitHub (pinned to 1774ccb71a)