···33# To re-generate a bundle for another specific version without changing the standard setup, you can:
44# - use the VERSION as arg of the bundle target (e.g make bundle VERSION=0.0.2)
55# - use environment variables to overwrite this value (e.g export VERSION=0.0.2)
66-VERSION ?= 0.6.36
66+VERSION ?= 0.6.37
7788# CHANNELS define the bundle channels used in the bundle.
99# Add a new line here if you would like to change its default config. (E.g CHANNELS = "candidate,fast,stable")
+2-2
helm/hsm-secrets-operator/Chart.yaml
···22name: hsm-secrets-operator
33description: A Kubernetes operator that bridges Pico HSM binary data storage with Kubernetes Secrets
44type: application
55-version: 0.6.36
66-appVersion: v0.6.36
55+version: 0.6.37
66+appVersion: v0.6.37
77icon: https://raw.githubusercontent.com/cncf/artwork/master/projects/kubernetes/icon/color/kubernetes-icon-color.svg
88home: https://github.com/evanjarrett/hsm-secrets-operator
99sources:
+12-4
internal/api/auth_integration_test.go
···6565 var response map[string]any
6666 err := json.Unmarshal(w.Body.Bytes(), &response)
6767 require.NoError(t, err)
6868- assert.Contains(t, response["error"], "invalid token")
6868+ errorObj, ok := response["error"].(map[string]any)
6969+ require.True(t, ok, "error should be an object")
7070+ assert.Contains(t, errorObj["message"], "invalid")
6971 })
70727173 t.Run("Missing Authorization Header", func(t *testing.T) {
···8183 var response map[string]any
8284 err := json.Unmarshal(w.Body.Bytes(), &response)
8385 require.NoError(t, err)
8484- assert.Contains(t, response["error"], "missing authorization header")
8686+ errorObj, ok := response["error"].(map[string]any)
8787+ require.True(t, ok, "error should be an object")
8888+ assert.Contains(t, errorObj["message"], "missing authorization header")
8589 })
86908791 t.Run("Malformed Authorization Header", func(t *testing.T) {
···98102 var response map[string]any
99103 err := json.Unmarshal(w.Body.Bytes(), &response)
100104 require.NoError(t, err)
101101- assert.Contains(t, response["error"], "invalid authorization header format")
105105+ errorObj, ok := response["error"].(map[string]any)
106106+ require.True(t, ok, "error should be an object")
107107+ assert.Contains(t, errorObj["message"], "invalid authorization header format")
102108 })
103109104110 t.Run("Health Endpoint Accessible Without Auth", func(t *testing.T) {
···141147 require.NoError(t, err)
142148143149 // Should contain details about the K8s token failure, not JWT auth failure
144144- assert.Contains(t, response["error"], "failed to generate token")
150150+ errorObj, ok := response["error"].(map[string]any)
151151+ require.True(t, ok, "error should be an object")
152152+ assert.Contains(t, errorObj["message"], "failed to generate")
145153 })
146154147155 t.Run("JWT Authentication Enabled", func(t *testing.T) {
+19-13
internal/hsm/pkcs11_cgo.go
···226226 if err := session.ctx.FindObjectsInit(session.session, template); err != nil {
227227 return nil, fmt.Errorf("failed to initialize object search: %w", err)
228228 }
229229- defer func() {
230230- if finalErr := session.ctx.FindObjectsFinal(session.session); finalErr != nil {
231231- // Ignore finalize error but continue
232232- _ = finalErr
233233- }
234234- }()
235229236230 // Get all matching objects
237237- objs, _, err := session.ctx.FindObjects(session.session, 1000) // Max 1000 objects
231231+ objs, _, err := session.ctx.FindObjects(session.session, 100) // Max 100 objects (reduced from 1000 to be less aggressive)
238232 if err != nil {
233233+ // Always finalize before returning error
234234+ _ = session.ctx.FindObjectsFinal(session.session)
239235 return nil, fmt.Errorf("failed to find objects: %w", err)
236236+ }
237237+238238+ // CRITICAL: Must call FindObjectsFinal to release search operation
239239+ // If this fails, the session remains in search mode and CreateObject will fail
240240+ if err := session.ctx.FindObjectsFinal(session.session); err != nil {
241241+ return nil, fmt.Errorf("failed to finalize object search (session may be in invalid state): %w", err)
240242 }
241243242244 // Pre-allocate slice for better performance
···334336 if err := session.ctx.FindObjectsInit(session.session, template); err != nil {
335337 return fmt.Errorf("failed to initialize object search: %w", err)
336338 }
337337- defer func() {
338338- if finalErr := session.ctx.FindObjectsFinal(session.session); finalErr != nil {
339339- // Ignore finalize error but continue
340340- _ = finalErr
341341- }
342342- }()
343339344340 // Get all matching objects
345341 objs, _, err := session.ctx.FindObjects(session.session, 100) // Max 100 objects
346342 if err != nil {
343343+ // Always finalize before returning error
344344+ _ = session.ctx.FindObjectsFinal(session.session)
347345 return fmt.Errorf("failed to find objects: %w", err)
348346 }
349347350348 // Delete each object that matches our path
349349+ deletedCount := 0
351350 for _, obj := range objs {
352351 // Get the label to check if this object matches our path
353352 labelAttr, err := session.ctx.GetAttributeValue(session.session, obj, []*pkcs11.Attribute{
···371370 // Log error but continue with other objects
372371 continue
373372 }
373373+ deletedCount++
374374+ }
375375+376376+ // CRITICAL: Must call FindObjectsFinal to release search operation
377377+ // If this fails, the session remains in search mode and CreateObject will fail
378378+ if err := session.ctx.FindObjectsFinal(session.session); err != nil {
379379+ return fmt.Errorf("failed to finalize object search after deleting %d objects (session may be in invalid state): %w", deletedCount, err)
374380 }
375381376382 return nil
+3-1
internal/hsm/pkcs11_client.go
···232232 "path", path, "keys", len(data))
233233234234 // First, delete any existing objects for this path to avoid duplicates
235235+ // IMPORTANT: Do not ignore errors here - if FindObjectsFinal fails in delete,
236236+ // the session will be in an invalid state and CreateObject will fail
235237 if err := deleteSecretObjectsPKCS11(c.session, path); err != nil {
236236- c.logger.V(1).Info("Failed to delete existing objects (may not exist)", "error", err)
238238+ return fmt.Errorf("failed to prepare HSM for write (delete existing objects): %w", err)
237239 }
238240239241 // Create data objects for each key-value pair
···270270 return fmt.Errorf("failed to read response body: %w", err)
271271 }
272272273273+ // Handle 401 Unauthorized - clear cached token
274274+ if resp.StatusCode == http.StatusUnauthorized {
275275+ if c.tokenManager != nil {
276276+ // Clear the cached token so next request gets a fresh one
277277+ _ = c.tokenManager.ClearCache()
278278+ }
279279+ }
280280+273281 var apiResp APIResponse
274282 if err := json.Unmarshal(respBody, &apiResp); err != nil {
275283 return fmt.Errorf("failed to parse API response: %w", err)
···278286 // Check if the API reported an error
279287 if !apiResp.Success {
280288 if apiResp.Error != nil {
281281- return fmt.Errorf("API error (%s): %s", apiResp.Error.Code, apiResp.Error.Message)
289289+ errMsg := fmt.Sprintf("API error (%s): %s", apiResp.Error.Code, apiResp.Error.Message)
290290+ // Add helpful hint for authentication errors
291291+ if resp.StatusCode == http.StatusUnauthorized {
292292+ errMsg += "\n\nAuthentication token was invalid or expired. The cached token has been cleared.\nPlease retry your command to authenticate with a fresh token."
293293+ }
294294+ return fmt.Errorf("%s", errMsg)
282295 }
283296 return fmt.Errorf("API request failed: %s", apiResp.Message)
284297 }
+1-1
kubectl-hsm/pkg/commands/auth.go
···164164 cmd.Flags().StringVar(&namespace, "namespace", "", "Namespace for the service account")
165165166166 return cmd
167167-}167167+}
+9-9
kubectl-hsm/pkg/commands/common.go
···181181 }
182182183183 ext := strings.ToLower(filepath.Ext(filename))
184184-184184+185185 // Auto-detect format based on file extension
186186 switch ext {
187187 case ".env":
···196196 if data, err := tryParseAsEnv(filename, content); err == nil {
197197 return data, nil
198198 }
199199-199199+200200 // Fall back to single key-value format
201201 return readAsSingleKeyValue(key, filename, content)
202202 }
···208208 if err := json.Unmarshal(content, &data); err != nil {
209209 return nil, err
210210 }
211211-211211+212212 // Check if this looks like secret data (not nested structures)
213213 for key, value := range data {
214214 if key == "" {
215215 return nil, fmt.Errorf("empty key found")
216216 }
217217-217217+218218 // Convert all values to strings, reject complex nested structures
219219 switch v := value.(type) {
220220 case string:
···229229 data[key] = fmt.Sprintf("%v", v)
230230 }
231231 }
232232-232232+233233 return data, nil
234234}
235235···237237func tryParseAsEnv(filename string, content []byte) (map[string]any, error) {
238238 lines := strings.Split(string(content), "\n")
239239 hasKeyValuePairs := false
240240-240240+241241 for _, line := range lines {
242242 line = strings.TrimSpace(line)
243243 if line == "" || strings.HasPrefix(line, "#") {
···248248 break
249249 }
250250 }
251251-251251+252252 if !hasKeyValuePairs {
253253 return nil, fmt.Errorf("no KEY=VALUE pairs found")
254254 }
255255-255255+256256 return readFromEnvContent(filename, content)
257257}
258258···323323 if key == "" {
324324 return nil, fmt.Errorf("empty key found in JSON file %s", filename)
325325 }
326326-326326+327327 // Convert non-string values to strings
328328 switch v := value.(type) {
329329 case string:
-1
kubectl-hsm/pkg/commands/create.go
···9494 return cmd
9595}
96969797-9897// RunWithCommandName executes the command with a specific command name for messaging
9998func (opts *CreateOptions) RunWithCommandName(ctx context.Context, secretName, commandName string) error {
10099 // Validate secret name