{"record":{"id":"cdef28bc4ccd843c","repo":"gastownhall/beads","slug":"identity-invalid-proxy-secret","errorCode":null,"errorMessage":"identity: invalid proxy secret","messagePattern":"identity: invalid proxy secret","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/dbproxy/identity/identity.go","lineNumber":62,"sourceCode":"\tif _, err := rand.Read(raw); err != nil {\n\t\treturn \"\", fmt.Errorf(\"identity: generate proxy secret: %w\", err)\n\t}\n\tsecret := hex.EncodeToString(raw)\n\tif err := atomicfile.WriteFile(filepath.Join(rootDir, SecretFileName), []byte(secret+\"\\n\"), 0o600); err != nil {\n\t\treturn \"\", fmt.Errorf(\"identity: write proxy secret: %w\", err)\n\t}\n\treturn secret, nil\n}\n\n// ReadSecret reads and validates the control-listener secret.\nfunc ReadSecret(rootDir string) (string, error) {\n\tdata, err := os.ReadFile(filepath.Join(rootDir, SecretFileName)) // #nosec G304 - rootDir is the workspace proxy root, not user input\n\tif err != nil {\n\t\treturn \"\", fmt.Errorf(\"identity: read proxy secret: %w\", err)\n\t}\n\tsecret := strings.TrimSpace(string(data))\n\tif len(secret) != 64 {\n\t\treturn \"\", errors.New(\"identity: invalid proxy secret\")\n\t}\n\tif _, err := hex.DecodeString(secret); err != nil {\n\t\treturn \"\", errors.New(\"identity: invalid proxy secret\")\n\t}\n\treturn secret, nil\n}\n","sourceCodeStart":44,"sourceCodeEnd":69,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/dbproxy/identity/identity.go#L44-L69","documentation":"ReadSecret loads the proxy secret file and requires the trimmed content to be exactly 64 characters — a 32-byte secret hex-encoded. This error means the file exists but its length is wrong: it was truncated, empty, or written in a different format (raw bytes, base64, extra content).","triggerScenarios":"Calling ReadSecret when SecretFileName under rootDir contains content whose trimmed length is not 64 — empty file, partial write during creation/rotation, or a secret written by a different tool/version in raw or base64 form.","commonSituations":"Crash or concurrent rotation leaving a partially written secret file; manually editing the secret file; provisioning tooling writing base64 instead of hex; disk-full truncation.","solutions":["Regenerate the secret with the package's WriteSecret/Rotate helper and restart the proxy","Check file size: expect 64 bytes (+optional newline); restore from backup if truncated","Ensure no two processes rotate the secret file concurrently (atomic rename, not in-place write)","Verify no provisioning script writes the secret in base64 or raw-byte form"],"exampleFix":"// before\n// hand-written secret\nos.WriteFile(secretPath, []byte(base64.StdEncoding.EncodeToString(key)), 0600)\n// after\n// hex-encode to exactly 64 chars, write atomically\nhexSecret := hex.EncodeToString(key) // 64 chars\nos.WriteFile(secretPath+\".tmp\", []byte(hexSecret+\"\\n\"), 0600)\nos.Rename(secretPath+\".tmp\", secretPath)","handlingStrategy":"validation","validationCode":"info, err := os.Stat(secretPath); if err != nil || info.Size() < 64 { return ErrSecretMissing }","typeGuard":"func isInvalidSecretLen(err error) bool { return strings.Contains(err.Error(), \"invalid proxy secret\") }","tryCatchPattern":"secret, err := identity.ReadSecret(root)\nif err != nil {\n    if isInvalidSecretLen(err) { return regenerateAndRotateSecret(root) }\n    return err\n}","preventionTips":["Only write the secret with the package's WriteSecret/Rotate helper","Use atomic writes (temp file + rename) to avoid truncated secrets","Ensure provisioning writes hex, not base64 or raw bytes","Check file size is 64 bytes (+newline) after any rotation"],"tags":["identity","secret-management","configuration","validation"],"backgroundTag":"invalid-secret","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}