Skip to content

feat(auth): add configurable secret backend for standalone auth - #1302

Draft
nbmaiti wants to merge 1 commit into
mainfrom
feat/jwt_sec_auth_keystore
Draft

nbmaiti wants to merge 1 commit into
mainfrom
feat/jwt_sec_auth_keystore

Conversation

@nbmaiti

@nbmaiti nbmaiti commented Oct 8, 2026 •

Copy link
Copy Markdown
Contributor

Summary

Add the configurable standalone authentication secret backend required by issue #1353.

  • Keep config as the default in v1.x and preserve its config/environment credential behavior.
  • Give OAuth2/OIDC precedence; standalone admin credential settings are ignored when AUTH_CLIENT_ID is set.
  • Support AUTH_SECRET_BACKEND=config|keyring.
  • In keyring mode, read and write admin credentials only in the OS keyring, bootstrap them on first run, and fail closed on partial or unavailable state. There is no migration or fallback to config/environment, and keyring setup does not rewrite config.yml.
  • Keep JWT signing keys out of the admin credential keyring.

Fixes #1353

Validation

  • go test ./...

@nbmaiti
nbmaiti requested a balanced review from Copilot October 8, 2026 06:26
@nbmaiti nbmaiti changed the title feat(auth): replace bcrypt with PBKDF2 for password hashing feat(auth): secure storage with PBKDF2 for password hashing Oct 8, 2026

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.

🔵 Needs a closer look

Security-sensitive migration gaps, plaintext credential logging, configuration regressions, and substantial unrelated scope require remediation and human review.

7 open findings
What changed in this PR

Migrates standalone authentication to PBKDF2-SHA256 while expanding credential storage, login protection, and configuration behavior.

Changes:

  • Adds PBKDF2 password generation and verification.
  • Introduces keyring-backed credential bootstrap and cleanup.
  • Adds login rate limiting, trusted proxies, tests, and API documentation.
File Description
pkg/​secrets/​pbkdf2.go Implements PBKDF2 hashing.
pkg/​secrets/​pbkdf2_test.go Tests PBKDF2 behavior.
internal/​controller/​ws/​v1/​redirect_test.go Synchronizes WebSocket tests.
internal/​controller/​openapi/​devices.go Documents login rate limiting.
internal/​controller/​httpapi/​v1/​login.go Verifies hashes and rate-limits logins.
internal/​controller/​httpapi/​v1/​login_test.go Updates authentication fixtures.
internal/​controller/​httpapi/​v1/​login_ratelimit_test.go Tests rate limiting.
internal/​app/​app.go Configures trusted proxies.
integration-test/​collections/​console_mps_apis.postman_collection.json Adds authorization coverage.
integration-test/​collections/​console_environment.postman_environment.json Adds admin variables.
go.sum Records dependency checksums.
go.mod Adds terminal support.
config/​config.yml Updates authentication defaults.
config/​config.go Changes credential and JWT configuration.
config/​config_test.go Tests JWT behavior.
cmd/​app/​secret_store.go Adds credential bootstrap and keyring storage.
cmd/​app/​main.go Integrates credential handling.
cmd/​app/​main_test.go Tests credential workflows.

🧠 Review effort: Balanced


Give feedback about Copilot approvals in this survey to enter a drawing for a $150 gift card.

Comment thread cmd/app/secret_store.go Outdated
Comment on lines +187 to +192
// Captured before resolution overwrites these fields below: a non-empty
// value here means config.yml itself holds a plaintext credential that
// must be cleared after it's persisted elsewhere. Resolving everything
// from the keyring/env instead must not force a (possibly read-only)
// config.yml rewrite that has nothing to clean up.
configFileHadCredentials := cfg.AdminUsername != "" || cfg.AdminPassword != "" || cfg.JWTKey != ""
Comment thread cmd/app/secret_store.go Outdated
Comment on lines +555 to +560
func normalizeAdminPasswordHash(password string) (hash string, converted bool, err error) {
if secrets.IsPBKDF2Hash(password) {
return password, false, nil
}

pbkdf2Hash, err := secrets.GeneratePBKDF2Hash(password)
Comment thread cmd/app/secret_store.go Outdated
Comment on lines +581 to +590
// Security: Log to stderr (not stdout) to prevent accidental credential exposure in stdout captures.
// Credentials should only be logged once during initial bootstrap for user visibility.
fmt.Fprintln(os.Stderr, "")
fmt.Fprintln(os.Stderr, "\033[31m===============================================\033[0m")
fmt.Fprintln(os.Stderr, "\033[31mCONSOLE ADMIN CREDENTIALS - FIRST-RUN BOOTSTRAP\033[0m")
fmt.Fprintln(os.Stderr, "\033[31m===============================================\033[0m")
fmt.Fprintln(os.Stderr, "\033[31mRecord these values now; they will NOT be shown again.\033[0m")
fmt.Fprintln(os.Stderr, "")
fmt.Fprintf(os.Stderr, " Admin Username: %s\n", "standalone")
fmt.Fprintf(os.Stderr, " Admin Password: %s\n", password)
Comment thread pkg/secrets/pbkdf2.go
Comment on lines +95 to +97
iterations, err := strconv.Atoi(parts[0])
if err != nil || iterations <= 0 {
return parsedPBKDF2Hash{}, false
Comment thread config/config.go
Comment on lines +85 to +93
// TrustedProxies lists the reverse proxy/gateway IPs or CIDRs allowed to
// set X-Forwarded-For/X-Real-IP for c.ClientIP() (used by login rate
// limiting and audit logs). Empty (the default) trusts none, so
// ClientIP() always reads the direct connection's RemoteAddr — safe
// against header spoofing, but means every client behind an untrusted
// proxy is grouped under that proxy's single address. Deployments that
// sit behind a real reverse proxy should list it here to get accurate
// per-client addresses instead.
TrustedProxies []string `yaml:"trusted_proxies" env:"HTTP_TRUSTED_PROXIES"`
Comment thread config/config.go
Comment on lines +468 to +478
key, err := GenerateRuntimeJWTKey()
if err != nil {
return err
return fmt.Errorf("config: generating auth.jwtKey: %w", err)
}

return updateConfigFile(configPath, func(c *Config) { c.AdminPassword = adminPassword })
cfg.JWTKey = key

log.Printf(
"WARNING: no auth.jwtKey was configured, so a runtime-only JWT key was generated for this process. " +
"For production, provide AUTH_JWT_KEY or auth.jwtKey explicitly, or use OAuth2/OIDC; local basic auth is intended for single-user deployments.",
)
Comment on lines +119 to +123
// The handshake response is already flushed by the time Redirect runs, but
// httptest.Server.Close() does not wait for goroutines still serving a
// hijacked (upgraded) connection, so without this the handler goroutine can
// outlive the test and race the next test's config.NewConfig() write.
var handlerWG sync.WaitGroup
@nbmaiti nbmaiti changed the title feat(auth): secure storage with PBKDF2 for password hashing feat(auth): add configurable secret backend Oct 28, 2026
@codecov

codecov Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 70.66667% with 88 lines in your changes missing coverage. Please review.
✅ Project coverage is 62.43%. Comparing base (36ac246) to head (93795fd).

Files with missing lines Patch % Lines
cmd/app/secret_store.go 59.79% 39 Missing ⚠️
config/config.go 69.49% 36 Missing ⚠️
internal/controller/httpapi/v1/login.go 85.36% 6 Missing ⚠️
pkg/secrets/pbkdf2.go 86.84% 5 Missing ⚠️
internal/app/app.go 33.33% 2 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1302      +/-   ##
==========================================
+ Coverage   62.32%   62.43%   +0.11%     
==========================================
  Files         159      161       +2     
  Lines       13435    13669     +234     
==========================================
+ Hits         8373     8534     +161     
- Misses       5062     5135      +73     

☔ 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.

@nbmaiti
nbmaiti force-pushed the feat/jwt_sec_auth_keystore branch 3 times, most recently from c88691c to 287303b Compare October 28, 2026 17:32
…tion

Support AUTH_SECRET_BACKEND=config|keyring while keeping config as the
 default throughout v1.x. Give OAuth2/OIDC precedence when AUTH_CLIENT_ID
 is configured and ignore standalone credential sources and backend settings.

Use only keyring credentials when keyring mode is selected, without
 reading, migrating, or modifying standalone credentials in configuration
 or environment values. Do not fall back between backends.

Fixes #1353

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
@nbmaiti nbmaiti changed the title feat(auth): add configurable secret backend feat(auth): add configurable secret backend for standalone auth Oct 28, 2026
@nbmaiti
nbmaiti force-pushed the feat/jwt_sec_auth_keystore branch from 287303b to 93795fd Compare October 28, 2026 17:55
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.

Add Configurable Secret Backend for Standalone Authentication

2 participants