Skip to content

feat(auth): replace bcrypt with PBKDF2 for password hashing - #1297

Draft
nbmaiti wants to merge 11 commits into
mainfrom
feat/jwt_auth_keystore
Draft

nbmaiti wants to merge 11 commits into
mainfrom
feat/jwt_auth_keystore

Conversation

@nbmaiti

@nbmaiti nbmaiti commented Oct 6, 2026

Copy link
Copy Markdown
Contributor

Description:

Migrate password hashing from bcrypt to PBKDF2-SHA256 for improved security and standards compliance.

Changes

Create new pkg/secrets/pbkdf2.go with PBKDF2 implementation (SHA256, 100k iterations, 16-byte salt)
Update password generation in cmd/app/secret_store.go
Update password verification in internal/controller/httpapi/v1/login.go
Update all test fixtures to use PBKDF2 functions
Remove all bcrypt dependencies from password handling code

Testing

✅ All existing tests pass
✅ Password generation and verification working correctly
✅ Login endpoint validated with new hashing
✅ Zero regressions

Security

  • PBKDF2-SHA256 with 100,000 iterations (OWASP recommended)
  • 128-bit random salt per password
  • Hash format supports future iteration count increases
  • No plaintext passwords in logs or storage

@nbmaiti
nbmaiti force-pushed the feat/jwt_auth_keystore branch from 73f0f9a to d0d3944 Compare October 6, 2026 07:08
@nbmaiti nbmaiti changed the title Feat/jwt auth keystore feat(auth): replace bcrypt with PBKDF2 for password hashing Oct 6, 2026
@codecov

codecov Bot commented Oct 6, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 69.85646% with 126 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.19%. Comparing base (0b9dd57) to head (3067e86).

Files with missing lines Patch % Lines
cmd/app/secret_store.go 77.13% 59 Missing ⚠️
config/config.go 31.16% 53 Missing ⚠️
internal/controller/httpapi/v1/login.go 82.35% 6 Missing ⚠️
pkg/secrets/pbkdf2.go 89.47% 4 Missing ⚠️
cmd/app/main.go 71.42% 2 Missing ⚠️
internal/app/app.go 33.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1297      +/-   ##
==========================================
+ Coverage   61.99%   62.19%   +0.20%     
==========================================
  Files         158      160       +2     
  Lines       13337    13679     +342     
==========================================
+ Hits         8268     8508     +240     
- Misses       5069     5171     +102     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

This comment was marked as outdated.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Security, keyring reliability, cleanup correctness, denial-of-service, and test race issues remain unresolved.

Review effort: Balanced
Findings: 4 High severity · 8 Medium severity · 2 Low severity

Open (14)
Previously missed (3)

In code that hasn't changed since last review

Medium severity Restore existing keyring values on write failure

cmd/​app/​secret_store.go:242

Deleting a successfully written entry is not a rollback when that key already existed. Because startup rewrites all three keyring values, a transient failure writing one value causes these branches to delete previously valid credentials and leave the installation partially configured. Snapshot and restore prior values, or avoid rewriting unchanged keyring credentials.

Medium severity Increase PBKDF2 work factor to 600,000 iterations

pkg/​secrets/​pbkdf2.go:16

PBKDF2-HMAC-SHA256's current OWASP work factor is 600,000 iterations, not 100,000. Using 100,000 makes newly stored admin credentials substantially cheaper to brute-force and does not meet the standards-compliance claim in this PR.

Low severity Split oversized authentication subsystem into focused changes

cmd/​app/​secret_store.go:1

This new 528-line subsystem combines keyring persistence, first-run bootstrap, .env parsing, JWT lifecycle, and a destructive cleanup CLI with the PBKDF2 migration. The repository requires focused 50–300-line PRs and prerequisite refactors to be split from the feature; separating these concerns is necessary to make the authentication change reviewable and reduce rollout risk.

Comment thread internal/controller/httpapi/v1/login.go

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comment thread cmd/app/secret_store.go Outdated
Comment thread pkg/secrets/pbkdf2.go Outdated
Store standalone admin credentials and the JWT signing key in the OS keyring.
Migrate legacy config values with bcrypt hashing and keep generated JWT keys
in process memory when they are not configured.

Preserve OAuth2 precedence and provide clean recovery for the complete local
credential set.
- Create new pkg/secrets/pbkdf2.go with PBKDF2 implementation
- Use SHA256 hash function with 100,000 iterations and 16-byte salt
- Replace bcrypt.GenerateFromPassword with GeneratePBKDF2Hash
- Replace bcrypt.CompareHashAndPassword with VerifyPBKDF2Hash
- Update all password hashing in cmd/app/secret_store.go
- Update password verification in internal/controller/httpapi/v1/login.go
- Update all test fixtures to use PBKDF2 functions
- Remove all bcrypt imports and dependencies from password handling code
- All existing tests pass with PBKDF2 implementation
- Run go mod tidy to regenerate go.sum
- Add missing golang.org/x/term entry (v0.46.0)
- Ensure all transitive dependencies are properly recorded
- Fixes CI error: missing go.sum entry for golang.org/x/term
- Add TestGeneratePBKDF2Hash: validates hash generation
- Add TestGeneratePBKDF2Hash_DifferentSalts: verifies unique salts
- Add TestVerifyPBKDF2Hash_ValidPassword: validates password verification
- Add TestVerifyPBKDF2Hash_InvalidPassword: verifies wrong password rejection
- Add TestVerifyPBKDF2Hash_NonPBKDF2Hash: validates non-PBKDF2 hash handling
- Add TestVerifyPBKDF2Hash_MalformedPBKDF2Hash: edge cases for malformed hashes
- Add TestIsPBKDF2Hash: validates hash type detection
- Add TestPBKDF2HashFormat: validates hash format compliance
- Add TestPBKDF2Consistency: ensures deterministic verification
- Achieves 86.7% coverage for pkg/secrets package
- Add 4 new test cases for normalizeAdminPasswordHash in cmd/app/main_test.go:
  - TestNormalizeAdminPasswordHash_EmptyString: verify empty password handling
  - TestNormalizeAdminPasswordHash_SpecialCharacters: verify special chars support
  - TestNormalizeAdminPasswordHash_VeryLongPassword: verify 10k char passwords
  - TestNormalizeAdminPasswordHash_UniqueHashes: verify random salt generation
- Improve pkg/secrets/pbkdf2_test.go with additional edge cases
- Achieve 86.7% test coverage for pkg/secrets package
- All tests passing with parallel execution
Fixes for:
- godot: Add period to format comment
- mnd: Extract magic number 3 to pbkdf2PartsCount constant
- paralleltest: Add t.Parallel() calls to subtest ranges
- tparallel: Add t.Parallel() to subtest definitions
- wsl_v5: Add missing blank lines for readability

All tests passing with parallel execution enabled
…eview

1. DoS Protection - Rate limiting on login endpoint
   - Track failed login attempts per client IP
   - Limit to 5 failures per 15 minute window
   - Return HTTP 429 (Too Many Requests) when exceeded
   - Thread-safe implementation using sync.Mutex
   - Automatically reset after timeout period
   - Resets on successful authentication

2. Credential Exposure Prevention
   - Redirect bootstrap credentials to stderr (not stdout)
   - Prevents accidental capture in stdout logs
   - Maintains user visibility for initial setup

3. Strict Hash Format Validation
   - Already implemented: validates complete PBKDF2 format
   - Checks: iterations (valid int), salt (hex), hash (hex)
   - Prevents misclassifying malformed strings as hashes

4. Test Serialization
   - Added //nolint:paralleltest to tests mutating global state
   - Prevents race conditions from concurrent test execution

All tests passing with proper synchronization and validation.
- Add blank line after loginMutex.Lock() before attempts assignment (wsl_v5)
- Add blank line before rate-limit return statement (nlreturn)
- Add blank line after loginMutex.Lock() before loginAttempts increment (wsl_v5)

All golangci-lint issues resolved in login.go
@nbmaiti
nbmaiti force-pushed the feat/jwt_auth_keystore branch from 0fea9f0 to b054ac4 Compare October 7, 2026 06:27
@nbmaiti
nbmaiti requested a balanced review from Copilot October 7, 2026 07:15

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Comment thread cmd/app/main_test.go Outdated
// Rate limiting: PBKDF2 with 100k iterations requires significant CPU.
// Unauthenticated clients attempting concurrent failed logins could saturate resources.
// Reject requests from clients with repeated auth failures (tracked per-IP).
clientIP := c.ClientIP()
Comment thread internal/controller/httpapi/v1/login.go Outdated
Comment thread internal/controller/httpapi/v1/login.go Outdated
Closes out the 16 still-open Copilot comments on the PBKDF2 migration:
timing-safe hash comparison and a higher iteration count, a misclassifying
IsPBKDF2Hash that could lock out an admin whose password started with the
hash prefix, a spoofable login rate-limit key via untrusted proxy headers,
a check-then-act race and unbounded growth in the login attempt limiter,
several admin-credential CLI bugs (dash handling, swallowed clean errors,
discarded JWT keys, unconditional config rewrites, mandatory keyring,
password trimming), a missing weak-password warning, and an empty
--config=/-config= value silently falling back to the default path.

Signed-off-by: Nabendu Maiti <nabendu.bikash.maiti@intel.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🟡 Changes recommended

Plaintext secrets can enter logs, configuration rewrites can break read-only deployments, and proxy-aware throttling and API contracts need correction.

Review effort: Balanced
Findings: 3 High severity · 4 Medium severity · 5 Low severity

Open (12)
Resolved since last review (13)

Comment thread cmd/app/secret_store.go
Comment thread internal/app/app.go Outdated
Comment on lines +79 to +82
// No reverse proxy is trusted by default, so c.ClientIP() (used for
// per-client login rate limiting) reads RemoteAddr directly instead of an
// attacker-controlled X-Forwarded-For/X-Real-IP header.
_ = handler.SetTrustedProxies(nil)
Comment on lines +176 to 180
if !reserveLoginAttempt(clientIP) {
c.JSON(http.StatusTooManyRequests, gin.H{errorKey: "rate_limited", messageKey: "Too many failed login attempts. Please try again later."})

return
}
Comment thread cmd/app/main.go
Comment on lines +67 to +74
handled, err := handleAdminCLI(os.Args[1:], newKeyringStorageFunc(), os.Stdout)
if err != nil {
log.Fatalf("Admin command error: %v", err)
}

if handled {
return
}
Comment on lines +176 to +177
if !reserveLoginAttempt(clientIP) {
c.JSON(http.StatusTooManyRequests, gin.H{errorKey: "rate_limited", messageKey: "Too many failed login attempts. Please try again later."})
Comment thread pkg/secrets/pbkdf2.go

const (
pbkdf2Prefix = "$pbkdf2$"
pbkdf2Iterations = 600000
#1297

Adds configurable http.trusted_proxies so ClientIP() isn't forced to
ignore every reverse proxy (previously SetTrustedProxies(nil) always,
collapsing all clients behind a real proxy into one rate-limit bucket),
covers the login rate limiter with tests for the attempt boundary, 429
response, successful-login reset, window expiry, and concurrent
reservations, and documents the new 429 outcome in both the OpenAPI
declaration and the Postman collection for /api/v1/authorize.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants