{"record":{"id":"135f9f6485a2d9bf","repo":"hashicorp/terraform","slug":"http-remote-state-endpoint-invalid-auth","errorCode":null,"errorMessage":"HTTP remote state endpoint invalid auth","messagePattern":"HTTP remote state endpoint invalid auth","errorType":"http","errorClass":null,"httpStatus":403,"severity":"error","filePath":"internal/backend/remote-state/http/client.go","lineNumber":101,"sourceCode":"\t}\n\tc.lockID = \"\"\n\n\tjsonLockInfo := info.Marshal()\n\tresp, err := c.httpRequest(c.LockMethod, c.LockURL, &jsonLockInfo, \"lock\")\n\tif err != nil {\n\t\treturn \"\", err\n\t}\n\tdefer resp.Body.Close()\n\n\tswitch resp.StatusCode {\n\tcase http.StatusOK:\n\t\tc.lockID = info.ID\n\t\tc.jsonLockInfo = jsonLockInfo\n\t\treturn info.ID, nil\n\tcase http.StatusUnauthorized:\n\t\treturn \"\", fmt.Errorf(\"HTTP remote state endpoint requires auth\")\n\tcase http.StatusForbidden:\n\t\treturn \"\", fmt.Errorf(\"HTTP remote state endpoint invalid auth\")\n\tcase http.StatusConflict, http.StatusLocked:\n\t\tdefer resp.Body.Close()\n\t\tbody, err := io.ReadAll(resp.Body)\n\t\tif err != nil {\n\t\t\treturn \"\", &statemgr.LockError{\n\t\t\t\tErr: fmt.Errorf(\"HTTP remote state already locked, failed to read body\"),\n\t\t\t}\n\t\t}\n\t\texisting := statemgr.LockInfo{}\n\t\terr = json.Unmarshal(body, &existing)\n\t\tif err != nil {\n\t\t\treturn \"\", &statemgr.LockError{\n\t\t\t\tErr: fmt.Errorf(\"HTTP remote state already locked, failed to unmarshal body\"),\n\t\t\t}\n\t\t}\n\t\treturn \"\", &statemgr.LockError{\n\t\t\tInfo: &existing,\n\t\t\tErr:  fmt.Errorf(\"HTTP remote state already locked: ID=%s\", existing.ID),","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/hashicorp/terraform/blob/c9def3e214014c1188faabfc4a5bde5095139765/internal/backend/remote-state/http/client.go#L83-L119","documentation":"During Lock(), the server returned 403 Forbidden: credentials were accepted but are not permitted to lock this state. Distinct from 401 (error 250) which is missing/unrecognized credentials.","triggerScenarios":"Valid user lacking the lock ACL on the workspace; correct token scoped read-only; path-level policy denying the LOCK method on the state endpoint. Fires at lock acquisition time during apply/plan/destroy.","commonSituations":"Service account rotated to lower privileges; new workspace with default restrictive policy; token environment variable pointing at a CI deploy token instead of a state-management token.","solutions":["Grant the configured principal permission to lock (and unlock) the state path on the server.","Verify with `curl -u user:pass -X LOCK <lock_address>` returns 200, not 403.","Switch to a credential that has the required lock/unlock ACLs."],"exampleFix":"// before: token scoped read-only\n// after: grant lock+unlock on the state path\n// policy on server (example):\n//   path \"state/*\" { capabilities = [\"create\",\"read\",\"update\",\"delete\",\"lock\",\"unlock\"] }","handlingStrategy":"validation","validationCode":"// Verify lock permission against the real endpoint before terraform apply\nfunc canLock(lockURL, user, pass string) error {\n  req, _ := http.NewRequest(\"LOCK\", lockURL, nil)\n  req.SetBasicAuth(user, pass)\n  resp, err := http.DefaultClient.Do(req)\n  if err != nil { return err }\n  defer resp.Body.Close()\n  if resp.StatusCode == http.StatusForbidden {\n    return fmt.Errorf(\"principal lacks lock permission\")\n  }\n  return nil\n}","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Grant the principal explicit lock+unlock ACLs on the state path.","Pre-flight the lock endpoint with curl in CI.","Use dedicated state-management credentials, not deploy-only tokens."],"tags":["auth","authorization","locking","http-backend","terraform"],"analyzedSha":"c9def3e214014c1188faabfc4a5bde5095139765","analyzedAt":"2026-08-07T15:39:49.278Z","schemaVersion":2},"datasetVersion":"2026-08-07T21:17:07.882Z"}