semaphoreui/semaphore · error

access key expired

Error message

access key expired

What it means

ErrAccessKeyExpired is returned by DeserializeSecret when the access key has an ExpireAt timestamp in the past. Expired secrets must never be usable, so any code path that materializes secret values (task dispatch, environment fill, remote job prep) fails with this sentinel so callers can distinguish expiry from other secret errors.

Solutions

  1. Extend or remove ExpireAt on the access key (or create a new key) and update tasks referencing it.
  2. Detect with errors.Is(err, ErrAccessKeyExpired) and surface a user-friendly 'key expired, run again' message (as the runner API does).
  3. Re-run the task after rotating the expired key.
  4. Set expiry policy on keys used by recurring tasks long enough to cover their schedule.

Example fix

// before
if err := encryptionService.DeserializeSecret(&key); err != nil { return err }
// after
if err := encryptionService.DeserializeSecret(&key); err != nil {
    if errors.Is(err, server.ErrAccessKeyExpired) {
        return fmt.Errorf("access key %q expired; please rotate it and re-run", key.Name)
    }
    return err
}
Defensive patterns

Strategy: try-catch

Validate before calling

if key.ExpireAt != nil && time.Now().After(*key.ExpireAt) {
    return fmt.Errorf("access key %q expired at %s", key.Name, key.ExpireAt)
}

Type guard

func keyUsable(k db.AccessKey) bool { return k.ExpireAt == nil || time.Now().Before(*k.ExpireAt) }

Try / catch

if err := encryptionService.DeserializeSecret(&key); err != nil {
    if errors.Is(err, server.ErrAccessKeyExpired) {
        tsk.Log("Survey secrets expired before the task started. Please run the task again.")
        return
    }
    return err
}

Prevention

When it happens

Trigger: DeserializeSecret(key) with key.ExpireAt != nil and tz.Now().After(*key.ExpireAt); reached from prepareRemoteJob, run, task survey secret gathering (api/runners/runners.go), and FillEnvironmentSecrets.

Common situations: Running a scheduled/old task whose access key TTL has elapsed; long-lived tasks re-reading secrets after the key expired; retrying a job that was queued past the key's expiry.

Related errors


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

Appendix: source

Thrown at services/server/access_key_encryption_svc.go:22

	"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
}

func NewAccessKeyEncryptionService(
	accessKeyRepo db.AccessKeyManager,
	environmentRepo db.EnvironmentManager,

View on GitHub (pinned to 1774ccb71a)