ory/hydra · error

cookiex: at least one secret is required

Error message

cookiex: at least one secret is required

What it means

cookiex.New requires at least one secret because the codec seals with a key derived from the first secret and opens with keys derived from all of them. With an empty secrets slice there is no key material, so the constructor immediately returns this error.

Source

Thrown at oryx/cookiex/cookiex.go:88

	keys    [][32]byte
	maxAge  time.Duration
	legacy  legacyState
	now     func() time.Time
}

// New returns a codec for the given purpose. The purpose is bound into the
// ciphertext and used as the metric label; it must be a short constant like
// "kratos/session". Because the purpose is embedded in the additional
// authenticated data, it must be non-empty and must not contain a pipe
// character. The codec seals with a key derived from the first secret
// and opens with keys derived from any of them, so secrets rotate by
// prepending a new one.
func New[T any](purpose string, secrets [][]byte, opts ...Option) (*Codec[T], error) {
	if purpose == "" || strings.Contains(purpose, "|") {
		return nil, errors.New("cookiex: purpose must be non-empty and must not contain a pipe character")
	}
	if len(secrets) == 0 {
		return nil, errors.New("cookiex: at least one secret is required")
	}
	cfg := config{maxAge: defaultMaxAge}
	for _, opt := range opts {
		opt(&cfg)
	}
	if cfg.maxAge < 0 {
		return nil, errors.New("cookiex: max age must not be negative")
	}
	if cfg.legacyEncode && len(cfg.legacyKeyPairs) == 0 {
		return nil, errors.New("cookiex: legacy encode requires legacy key pairs")
	}
	keys := make([][32]byte, len(secrets))
	for i, secret := range secrets {
		key, err := hkdf.Key(sha256.New, secret, nil, kdfInfo, 32)
		if err != nil {
			return nil, errors.Wrap(err, "cookiex: cannot derive key")
		}
		keys[i] = [32]byte(key)

View on GitHub (pinned to 4174065ffb)

Solutions

  1. Provide at least one secret (ideally 32+ random bytes) at startup, e.g. generate with crypto/rand and store in a secret manager.
  2. Fail fast at boot: validate the secrets config before constructing the codec and surface a clear config error.
  3. Seed from the existing system secret (e.g. Hydra's system secret) if the app should share key material.
  4. In tests, always pass a dummy secret slice to New.

Example fix

// before
secrets := [][]byte{}
codec, err := cookiex.New[Session]("session", secrets) // panics-free but errors
// after
secrets := [][]byte{[]byte(os.Getenv("COOKIE_SECRET"))}
if len(secrets[0]) == 0 {
    log.Fatal("COOKIE_SECRET env var must be set")
}
codec, err := cookiex.New[Session]("session", secrets)
Defensive patterns

Strategy: validation

Validate before calling

// validate secrets before constructing the codec
func validateSecrets(secrets [][]byte) error {
    if len(secrets) == 0 {
        return errors.New("at least one cookie secret must be configured")
    }
    for i, s := range secrets {
        if len(s) < 32 {
            return fmt.Errorf("cookie secret %d too short (<32 bytes)", i)
        }
    }
    return nil
}

Try / catch

codec, err := cookiex.New[Session](purpose, secrets)
if err != nil {
    log.Fatalf("cookie codec setup failed: %v", err) // fail fast at boot
}

Prevention

When it happens

Trigger: Calling New[T](purpose, nil) or New[T](purpose, [][]byte{}) — typically when secrets come from config/environment and the list is empty (oryx/cookiex.go:88).

Common situations: Missing or empty SESSION_COOKIE_SECRETS-like env var; secrets YAML key present but with no items; a migration that changed the config schema so the loader returns an empty slice; tests constructing a codec before injecting test secrets.

Related errors


AI-assisted analysis of ory/hydra@4174065ffb (2026-09-03). Data as JSON: /api/errors/9b6f3abdcac13109. Report an issue: GitHub.