Skip to content

fix(auth): secure non-OAuth2 admin credentials with keyring-first flow and runtime JWT key - #1161

Open
nbmaiti wants to merge 1 commit into
mainfrom
jwt_auth_keystore
Open

fix(auth): secure non-OAuth2 admin credentials with keyring-first flow and runtime JWT key#1161
nbmaiti wants to merge 1 commit into
mainfrom
jwt_auth_keystore

Conversation

@nbmaiti

@nbmaiti nbmaiti commented Jul 27, 2026

Copy link
Copy Markdown

Summary

This PR implements secure credential handling for non-OAuth2 Console deployments, aligned with issue #1067.

It centralizes admin secret handling, prioritizes keyring-backed storage, removes default JWT key placeholders from config templates, and ensures runtime JWT key generation when no explicit key is configured.

Related Issue

What Changed

Admin credential flow (non-OAuth2)

  • Centralized admin credential logic into a dedicated secret-store module.
  • Added keyring-first credential resolution with fallback order:
    1. keyring
    2. .env
    3. environment variables
    4. config.yml
    5. interactive prompt
  • Added admin CLI workflows:
    • --add-admin
    • --remove-admin
    • --show-admin (username only; password is never displayed)
  • On successful keyring save, admin credentials are scrubbed from config.yml.
  • If keyring save fails after interactive input, user confirmation is required before persisting fallback values to config.

Password security

  • Admin passwords are normalized to bcrypt hashes before persistence.
  • Login path now verifies credentials using bcrypt hash comparison (no plaintext equality check).

JWT key behavior

  • Removed default placeholder JWT key values from templates.
  • AUTH_JWT_KEY is no longer treated as required.
  • If no JWT key is configured, Console generates a runtime-only key in memory at startup.

Files Changed

  • .env.example
  • cmd/app/main.go
  • cmd/app/main_test.go
  • cmd/app/secret_store.go
  • config/config.go
  • config/config.yml
  • config/config_test.go
  • internal/controller/httpapi/v1/login.go
  • internal/controller/httpapi/v1/login_test.go

Validation

Executed:

  • go test ./config
  • go test ./cmd/app ./internal/controller/httpapi/v1

All passed.

Notes

  • No infrastructure/docker-compose changes are included in this PR.
  • Remote secret store (for example Vault-backed admin/JWT secret retrieval) remains

@nbmaiti
nbmaiti requested a review from Copilot July 27, 2026 15:58
@nbmaiti
nbmaiti requested a review from a team as a code owner July 27, 2026 15:58
@codecov

codecov Bot commented Jul 27, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 55.16014% with 126 lines in your changes missing coverage. Please review.
✅ Project coverage is 44.65%. Comparing base (f0e57a4) to head (f108807).

Files with missing lines Patch % Lines
cmd/app/secret_store.go 58.64% 75 Missing and 23 partials ⚠️
config/config.go 33.33% 21 Missing and 3 partials ⚠️
cmd/app/main.go 33.33% 2 Missing and 2 partials ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1161      +/-   ##
==========================================
+ Coverage   44.45%   44.65%   +0.19%     
==========================================
  Files         144      145       +1     
  Lines       13732    14008     +276     
==========================================
+ Hits         6105     6255     +150     
- Misses       7054     7152      +98     
- Partials      573      601      +28     

☔ 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 jwt_auth_keystore branch from a827972 to d29099e Compare July 27, 2026 16:02

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.

Pull request overview

This PR strengthens non-OAuth2 authentication by moving admin credential handling into a keyring-first flow, hashing admin passwords with bcrypt, and ensuring the JWT signing key is generated at runtime when not explicitly configured.

Changes:

  • Introduces a centralized admin secret-store flow (keyring → .env → env vars → config.yml → prompt) plus CLI helpers for managing stored admin credentials.
  • Switches basic-auth verification from plaintext comparison to bcrypt hash verification.
  • Removes placeholder JWT keys from templates and generates an in-memory JWT key at startup when none is configured.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
.env.example Removes placeholder auth secrets and documents keystore-first resolution.
cmd/app/main.go Wires admin CLI handling and replaces legacy admin password setup with the new credential flow.
cmd/app/main_test.go Adds unit tests for the new secret-store resolution, CLI commands, and hashing behavior.
cmd/app/secret_store.go Implements keyring-first admin credential resolution, interactive prompting, and CLI workflows.
config/config.go Makes AUTH_JWT_KEY optional and generates a runtime JWT key when missing; adds helpers to clear/persist admin creds.
config/config.yml Removes default admin/JWT placeholders and adds guidance comments for secure configuration.
config/config_test.go Adds tests around runtime JWT key behavior.
internal/controller/httpapi/v1/login.go Uses bcrypt hash comparison for basic-auth login verification.
internal/controller/httpapi/v1/login_test.go Updates tests to use bcrypt-hashed admin password.
Comments suppressed due to low confidence (3)

internal/controller/httpapi/v1/login.go:83

  • The || short-circuit means bcrypt is only evaluated when the username matches, which makes username enumeration possible via response-time differences (bcrypt is much slower than a simple string compare). Compute the password check first so the handler always does the bcrypt work regardless of username match.
	if creds.Username != lr.Config.AdminUsername || bcrypt.CompareHashAndPassword([]byte(lr.Config.AdminPassword), []byte(creds.Password)) != nil {
		c.JSON(http.StatusUnauthorized, gin.H{errorKey: "invalid credentials", messageKey: "Incorrect Username and/or Password!"})

		return
	}

cmd/app/secret_store.go:174

  • If only one of the two keystore writes succeeds, the keystore can end up in a partial state that later causes confusing credential resolution (e.g., username from keyring + password from another source). Consider a best-effort rollback when either write fails.
	usernameSaveErr := keyringStore.SetKeyValue(keyringAdminUsername, cfg.AdminUsername)
	passwordSaveErr := keyringStore.SetKeyValue(keyringAdminPassword, cfg.AdminPassword)

	if usernameSaveErr == nil && passwordSaveErr == nil {
		// Credentials safely in keystore; scrub any plaintext from config.yml.
		if err := config.ClearAdminCredentials(); err != nil {
			log.Printf("Warning: failed to clear admin credentials from config.yml: %v", err)
		} else {
			log.Print("Admin credentials cleared from config.yml (stored in keystore).")
		}

		return
	}

cmd/app/main_test.go:183

  • This test uses t.Setenv, which mutates process-global environment variables and should not run in parallel. Add an explicit //nolint:paralleltest (as used elsewhere in this repo) to avoid paralleltest linter failures.
func TestResolveAdminCredentialsFromSources_FallbackToDotEnvThenConfig(t *testing.T) {
	t.Setenv(authAdminUsernameEnv, "")
	t.Setenv(authAdminPasswordEnv, "")

Comment thread cmd/app/secret_store.go Outdated
Comment thread cmd/app/secret_store.go Outdated
Comment thread cmd/app/secret_store.go
Comment thread config/config_test.go
Comment thread cmd/app/main_test.go
@nbmaiti
nbmaiti force-pushed the jwt_auth_keystore branch 3 times, most recently from 4ee2bf3 to f914325 Compare July 28, 2026 02:59
@nbmaiti
nbmaiti requested a review from Copilot July 28, 2026 06:10
@nbmaiti nbmaiti self-assigned this Jul 28, 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.

Pull request overview

Copilot reviewed 10 out of 11 changed files in this pull request and generated 1 comment.

Comments suppressed due to low confidence (4)

cmd/app/main_test.go:164

  • This test uses t.Setenv but doesn't call t.Parallel(). With paralleltest enabled, new tests are expected to either call t.Parallel() or explicitly opt out. Since environment mutation isn't safe to run in parallel, add a //nolint:paralleltest on the test declaration (per CLAUDE.md:245).
func TestResolveAdminCredentialsFromSources_PriorityOrder(t *testing.T) {
	t.Setenv(authAdminUsernameEnv, "env-user")
	t.Setenv(authAdminSecretEnv, "env-pass")

cmd/app/main_test.go:183

  • This test uses t.Setenv but doesn't call t.Parallel(). With paralleltest enabled, new tests should either call t.Parallel() or explicitly opt out. Since environment mutation isn't safe to run in parallel, add a //nolint:paralleltest on the test declaration (per CLAUDE.md:245).
func TestResolveAdminCredentialsFromSources_FallbackToDotEnvThenConfig(t *testing.T) {
	t.Setenv(authAdminUsernameEnv, "")
	t.Setenv(authAdminSecretEnv, "")

cmd/app/secret_store.go:178

  • handleAdminCredentials runs even when OAuth2 is configured (auth.clientId set). In that mode Console doesn't use basic-auth admin credentials, but this function can still prompt/exit on EOF in non-interactive environments, breaking OAuth2 deployments unnecessarily. Skip admin credential resolution when OAuth2 is enabled (same condition used in login.go: ClientID != "").
func handleAdminCredentials(cfg *config.Config) {
	if cfg.Disabled {
		log.Print("Auth is disabled; skipping admin credential resolution.")

		return

go.mod:74

  • go.mod marks golang.org/x/term as indirect, but it's imported directly (cmd/app/secret_store.go). This usually indicates go.mod wasn't tidied; leaving it indirect can cause churn on the next go mod tidy run.
	golang.org/x/term v0.45.0 // indirect

Comment thread config/config.go
Comment on lines +338 to +342
var configPathFlag string
if f := flag.Lookup("config"); f != nil {
configPathFlag = f.Value.String()
}

@nbmaiti nbmaiti added the bug Something isn't working label Jul 28, 2026
@nbmaiti nbmaiti moved this to In Progress in Sprint Planning Jul 29, 2026
@nbmaiti nbmaiti moved this to Q3 2025 in Backlog and Roadmap Jul 29, 2026
@nbmaiti nbmaiti removed the status in Sprint Planning Jul 29, 2026
@nbmaiti nbmaiti moved this to In Progress in Sprint Planning Jul 29, 2026
@nbmaiti
nbmaiti force-pushed the jwt_auth_keystore branch from f914325 to f108807 Compare July 29, 2026 03:33
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Status: Q3 2025
Status: In Progress

Development

Successfully merging this pull request may close these issues.

Provide secure credential storage for non-OAuth2 Console deployments

2 participants