semaphoreui/semaphore · error

invalid ssh key

Error message

invalid ssh key

What it means

SerializeSecret for a local-stored SSH-type access key requires consistency: if there is no private key, the key may not carry a login or passphrase either — such a key is meaningless. Instead of silently saving a hollow SSH key, SerializeSecret returns 'invalid ssh key' so RekeyAccessKeys fails loudly.

Solutions

  1. Provide the SshKey.PrivateKey value for the access key before serializing.
  2. Clear SshKey.Login and SshKey.Passphrase if the key is intentionally key-less, or change the key type to LoginPassword/None.
  3. Fix the client code that constructs the AccessKey so a complete SshKey is always submitted.

Example fix

// before
key.SshKey = db.SshKey{Login: "deploy"}
// after
key.SshKey = db.SshKey{PrivateKey: privPEM, Login: "deploy", Passphrase: "..."}
Defensive patterns

Strategy: validation

Validate before calling

if key.Type == db.AccessKeySSH && key.SshKey.PrivateKey == "" && (key.SshKey.Login != "" || key.SshKey.Passphrase != "") {
    // invalid: supply PrivateKey or clear Login/Passphrase
}

Try / catch

if err := svc.SerializeSecret(key); err != nil && err.Error() == "invalid ssh key" { /* fix key fields before retry */ }

Prevention

When it happens

Trigger: Calling SerializeSecret (typically during RekeyAccessKeys re-encryption) on an AccessKey of type AccessKeySSH whose SshKey.PrivateKey is empty while SshKey.Login or SshKey.Passphrase is non-empty.

Common situations: Partial key creation where the private key upload failed but login/passphrase fields were saved; API clients sending SSH keys with only login/passphrase; data corrupted by partial updates.

Understand the failure class

Background: "must not be empty", "cannot be empty" — required-field validation errors across open-source libraries — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at services/server/access_key_serializer_local.go:41

	// No-op for local deserializer
	return nil
}

func (d *LocalAccessKeyDeserializer) SerializeSecret(key *db.AccessKey) error {
	var plaintext []byte
	var err error

	switch key.Type {
	case db.AccessKeyString:
		if key.String == "" {
			key.Secret = nil
			return nil
		}
		plaintext = []byte(key.String)
	case db.AccessKeySSH:
		if key.SshKey.PrivateKey == "" {
			if key.SshKey.Login != "" || key.SshKey.Passphrase != "" {
				return fmt.Errorf("invalid ssh key")
			}
			key.Secret = nil
			return nil
		}

		plaintext, err = json.Marshal(key.SshKey)
		if err != nil {
			return err
		}
	case db.AccessKeyLoginPassword:
		if key.LoginPassword.Password == "" {
			if key.LoginPassword.Login != "" {
				return fmt.Errorf("invalid password key")
			}
			key.Secret = nil
			return nil
		}

View on GitHub (pinned to 1774ccb71a)