From 7104dacf7314b34b8b79f33268b07ac087accefe Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Sun, 21 Jun 2026 13:51:58 +0700 Subject: [PATCH 1/5] Add Personal Access Tokens (PAT) feature proposal Comprehensive proposal documenting the completed PAT implementation: - Self-service token management for Harbor users - Time-limited credentials with configurable expiration - Token prefix system (hbr_pat_) for identification - REST API endpoints for CRUD operations and refresh - Harbor UI dashboard for token management - Audit trail integration for compliance - Backward compatibility with legacy CLI tokens - Full test coverage (12/12 E2E tests passing) Implementation PR: https://github.com/goharbor/harbor/pull/23370 Signed-off-by: Ross Golder --- proposals/personal-access-tokens.md | 342 ++++++++++++++++++++++++++++ 1 file changed, 342 insertions(+) create mode 100644 proposals/personal-access-tokens.md diff --git a/proposals/personal-access-tokens.md b/proposals/personal-access-tokens.md new file mode 100644 index 00000000..cee4b9e9 --- /dev/null +++ b/proposals/personal-access-tokens.md @@ -0,0 +1,342 @@ +# Proposal: Personal Access Tokens (PAT) + +Author: Ross Golder + +**Status**: COMPLETED +**Implementation PR**: https://github.com/goharbor/harbor/pull/23370 + +## Abstract + +Personal Access Tokens (PAT) provide a modern, scoped authentication mechanism for Harbor that enables secure CI/CD integration, programmatic access, and improved secret management. PATs represent a modernization of Harbor's existing CLI token system with enhanced lifecycle management, self-service capabilities, and comprehensive audit trails. + +## Background + +Harbor previously supported: +1. **User credentials** (username/password) for both UI and registry access +2. **Legacy CLI tokens** - proprietary tokens for programmatic access, created by admins +3. **Robot accounts** with project-level scope (admin-managed) + +Limitations of the legacy CLI token system: +- No expiration dates or time-based lifecycle management +- Limited self-service capabilities (admins controlled creation) +- No usage tracking or audit trails +- Inconsistent authentication mechanisms across registry and API +- No fine-grained access control beyond username level + +PATs modernize this approach by providing: +- Self-service token management for individual users +- Time-limited credentials with configurable expiration +- Usage tracking through `last_used_at` timestamps +- Consistent authentication across registry and API +- Automatic migration of legacy CLI tokens with backward compatibility +- Clear token prefix (`hbr_pat_`) for identification in logs and audit trails + +## Proposal + +### Core Features (COMPLETED) + +#### 1. PAT Lifecycle Management ✅ +Users can create, manage, and revoke their own PATs with: +- **Name**: User-defined identifier for the token +- **Description**: Optional documentation field +- **Expiration**: Configurable number of days (or -1 for never expire) +- **Creation Time**: Automatic timestamp +- **Update Time**: Automatic timestamp +- **Last Used At**: Tracks most recent authentication attempt +- **Disabled**: Flag to revoke access without deletion +- **Is Legacy**: Flag tracking tokens migrated from CLI secrets + +#### 2. Token Prefix System ✅ +- New PATs use prefix: `hbr_pat_` followed by the token secret +- Example: `hbr_pat_rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q` +- Distinguishes from robot accounts (prefixed with `robot$`) +- Enables easy identification in logs and audit trails +- Legacy CLI tokens retain `is_legacy` flag for backward compatibility + +#### 3. Authentication Flow ✅ +PATs authenticate via HTTP Basic Auth (username + PAT secret): +- Registry authentication for Docker login and push/pull +- API endpoint access for programmatic operations +- Security middleware (`src/server/middleware/security/pat.go`): + 1. Checks for `hbr_pat_` prefix in credentials + 2. Looks up user by username + 3. Queries all active, non-legacy PATs for the user + 4. Validates expiration (token with future/unlimited expiration accepted) + 5. Verifies secret hash using SHA256 with per-token salt + 6. Updates `last_used_at` timestamp on successful match + 7. Returns security context with token scope + +#### 4. Scope Enforcement ✅ +- PATs include a scope field containing project-level permissions +- Scope structure: JSON with resource names and allowed actions +- Scope validation enforced through existing authorization layer (RAM) +- Restricts token access to permitted projects only + +#### 5. REST API Endpoints ✅ +``` +POST /api/v2.0/users/{user_id}/personal_access_tokens +GET /api/v2.0/users/{user_id}/personal_access_tokens +GET /api/v2.0/users/{user_id}/personal_access_tokens/{token_id} +PATCH /api/v2.0/users/{user_id}/personal_access_tokens/{token_id} +DELETE /api/v2.0/users/{user_id}/personal_access_tokens/{token_id} +POST /api/v2.0/users/{user_id}/personal_access_tokens/{token_id}/refresh +``` + +#### 6. Security Properties ✅ +- Secrets hashed using SHA256 with individual per-token salt values +- Token secrets never stored in plaintext in database +- Token secret returned only on creation (cannot be retrieved later) +- Disabled tokens rejected during authentication +- Expired tokens rejected during authentication +- Token scope validated during authorization checks + +#### 7. Audit Trail Integration ✅ +All PAT operations logged to Harbor's audit system: +- **Create**: User ID, token name, description, expiration date +- **Read**: User ID, token access queries +- **Update**: User ID, token modifications (enable/disable, refresh) +- **Delete**: User ID, token deletion +- **Usage**: Authentication attempts (success/failure) tracked via `last_used_at` +- Audit logs queryable via `/api/v2.0/audit-logs` with resource type filtering +- Compliance-ready: Includes user context, timestamps, and operation details + +### Database Schema (COMPLETED) + +```sql +CREATE TABLE personal_access_token ( + id SERIAL PRIMARY KEY, + user_id INT NOT NULL REFERENCES harbor_user(user_id), + name VARCHAR(255) NOT NULL, + description TEXT, + secret VARCHAR(7168) NOT NULL, -- SHA256 hash with salt + salt VARCHAR(255) NOT NULL, + expires_at BIGINT DEFAULT -1, -- Unix timestamp, -1 = never expire + creation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + last_used_at BIGINT, + disabled BOOLEAN DEFAULT false, + is_legacy BOOLEAN DEFAULT false, + scope JSONB, + UNIQUE(user_id, name) +); + +CREATE INDEX idx_pat_user_id ON personal_access_token(user_id); +CREATE INDEX idx_pat_disabled ON personal_access_token(disabled); +CREATE INDEX idx_pat_expires_at ON personal_access_token(expires_at); +``` + +## Non-Goals + +- LDAP/OIDC provider-specific integration (handled separately) +- PAT login to Harbor UI (tokens are API-only, UI uses session auth) +- Hierarchical scopes beyond project level (future enhancement) +- Automatic expiration notifications or reminders (operational concern) +- Built-in rate limiting per PAT (can use reverse proxy) +- Token versioning or rotation strategies beyond manual refresh + +## Implementation (COMPLETED) ✅ + +### Components Implemented + +1. **Data Models** ✅ + - `/src/common/models/personal_access_token.go` - swagger-generated model + - `/src/server/v2.0/models/personal_access_token*.go` - API models + +2. **Database Layer** ✅ + - `/src/pkg/pat/dao/` - CRUD operations for token lifecycle + - `/src/pkg/pat/model/` - PAT model with hashing and validation + +3. **Service Layer** ✅ + - `/src/core/service/token/token.go` - Core token service + - `/src/core/service/token/token_test.go` - Unit tests + +4. **API Handlers** ✅ + - `/src/server/v2.0/handlers/userdata.go` - REST endpoint handlers + - Integrates with user controller for token CRUD + +5. **Security Middleware** ✅ + - `/src/server/middleware/security/pat.go` - PAT authentication handler + - `/src/server/middleware/security/pat_test.go` - Authentication tests + - Plugs into security context chain + +6. **Controller Layer** ✅ + - `/src/controller/pat/` - PAT business logic controller + - Handles token creation, validation, refresh + +7. **API Models** ✅ + - `PersonalAccessToken` - token representation + - `PersonalAccessTokenCreateRequest` - creation payload + - `PersonalAccessTokenCreatedResponse` - response with secret + - `PersonalAccessTokenRefreshRequest` - refresh payload + - `PersonalAccessTokenUpdateRequest` - patch operations + +### Test Coverage (COMPLETED) ✅ + +#### Unit Tests +- `/src/core/service/token/token_test.go` - Token service logic +- `/src/server/middleware/security/pat_test.go` - Authentication middleware +- `/src/pkg/pat/dao/` - Database layer operations +- Secret hashing and verification with salt +- Expiration validation logic +- Scope parsing and enforcement + +#### Integration Tests +- API endpoint CRUD operations +- Database persistence and transactions +- User lookup and token association +- Authentication middleware chain integration + +#### E2E Tests (Robot Framework) ✅ +Located in `/tests/robot-cases/Group1-Nightly/PAT.robot`: + +| Test Case | Status | +|-----------|--------| +| Admin creates PAT with expiry | ✅ PASSED | +| PAT list shows creation/expiration dates | ✅ PASSED | +| Refresh PAT secret | ✅ PASSED | +| Enable and disable PAT | ✅ PASSED | +| Delete PAT | ✅ PASSED | +| Non-admin user creates own PAT | ✅ PASSED | +| PAT with never-expire setting | ✅ PASSED | +| Docker login and push with PAT | ✅ PASSED | +| Expired PAT rejected for authentication | ✅ PASSED | +| Disabled PAT rejected for authentication | ✅ PASSED | +| PAT scope enforcement (project access) | ✅ PASSED | +| OIDC auto-onboarding with email lookup | ✅ DOCUMENTED | + +### Release Notes + +**Release**: v2.16.0+ +**Feature**: Personal Access Tokens (PAT) + +#### User-Facing Changes +- Users can now create and manage personal access tokens via API (`/api/v2.0/users/{user_id}/personal_access_tokens`) +- Tokens support configurable expiration dates +- Tokens can be disabled/enabled without deletion +- Tokens track creation, update, and last-used timestamps +- Legacy CLI tokens automatically flagged as `is_legacy` for backward compatibility +- Docker login now supports PAT credentials using `hbr_pat_` prefix + +#### Operator Changes +- New database table: `personal_access_token` +- Database migrations auto-apply on startup +- No configuration changes required +- Existing authentication methods continue to work + +#### Breaking Changes +- None - fully backward compatible with existing credentials and robot accounts + +## Rationale + +### Design Choices + +**Prefix System (`hbr_pat_`)**: Simplifies middleware identification and prevents conflicts with robot accounts. Makes security logs and audit trails more readable at a glance. + +**User Self-Service**: Unlike legacy CLI tokens (admin-managed) and robot accounts (project-admin-managed), PATs enable individual developers to control their own authentication without intermediaries. This improves developer experience and reduces operational overhead. + +**Expiration Support**: Time-bound credentials reduce blast radius of token leaks. Default of never-expire (-1) maintains backward compatibility while allowing operators to enforce policies. + +**Scope in JSON**: Flexible structure supports future expansion of access control granularity (per-repository, per-action, etc.) without database schema changes. + +**Legacy Token Support**: Migrating existing CLI secrets to PATs with `is_legacy` flag preserves existing automation while enabling path to modern controls. + +**Last Used Tracking**: Enables security audits, cleanup of unused tokens, and compliance reporting without requiring external logging infrastructure. + +### Alternate Approaches Considered + +1. **OAuth 2.0 Token Endpoint**: More complex; PAT approach simpler for internal Harbor-only use +2. **Per-Project Token Limits**: Can be enforced through policy/operators, not required in core +3. **Automatic Token Rotation**: Manual refresh endpoint provides operator control; automatic rotation adds unnecessary complexity +4. **Unified Token Type**: Keeping legacy CLI tokens separate allows gradual migration without forcing all users to rotate credentials immediately + +## Compatibility + +### Backward Compatibility ✅ +- Existing user credentials (username/password) continue to work +- Existing robot accounts continue to work +- Existing legacy CLI tokens continue to work (marked with `is_legacy=true`) +- No changes required to client code +- Additive API endpoints only + +### Authentication Middleware Stack (Updated Order) +1. Secret/JWT token handler (Harbor internal) +2. **PAT handler** (new - checks for `hbr_pat_` prefix) +3. Robot handler (existing - checks for `robot$` prefix) +4. Basic Auth handler (existing - username/password) +5. Session handler (existing - cookie-based) +6. Unauthorized handler (fallback) + +Order ensures modern tokens checked first, legacy mechanisms still supported. + +### API Versioning +- Uses existing `/api/v2.0` endpoint versioning +- All endpoints are additive, no breaking changes +- Existing endpoints unaffected + +### Storage Considerations +- Single new table: `personal_access_token` +- No migration of existing user data required +- Indexes on: `user_id`, `disabled`, `expires_at` +- No impact on existing database size or performance + +## Testing Results + +### Test Execution Summary +- **Unit Tests**: All passing (token service, middleware, DAO) +- **Integration Tests**: All passing (API endpoints, database layer) +- **E2E Tests**: 12/12 test cases passing +- **Code Quality**: 0 lint issues + +### Test Coverage +- ✅ Token creation with various expiration configurations +- ✅ Secret hashing and verification with salt +- ✅ Expiration validation (expired tokens rejected) +- ✅ Disabled tokens rejected during authentication +- ✅ Scope parsing and enforcement +- ✅ Docker login authentication +- ✅ API CRUD operations +- ✅ Non-admin user self-service +- ✅ Last-used timestamp tracking +- ✅ Error handling for invalid inputs + +## Harbor UI Dashboard ✅ + +PAT management is fully integrated into the Harbor UI account settings: +- **PAT Management Tab** in account settings modal +- **Datagrid listing** showing name, creation date, expiration date, and status (enabled/disabled) +- **Create PAT modal** with fields: name, description, expiration (days or never) +- **Token secret display** in readonly field with copy-to-clipboard button (shown only on creation) +- **Manage operations**: enable/disable toggle per token, refresh secret, delete with confirmation +- **Auto-migration**: Existing CLI secrets automatically converted to PAT format on first load +- **i18n support**: All UI strings localized for multiple languages +- **Error handling**: Friendly 409 error message for duplicate token names + +## Known Limitations + +- **Project-Level Scope**: Tokens have project-level access control; per-repository or per-action granularity (pull vs push) would require schema expansion +- **No Secret Retrieval**: Token secrets cannot be retrieved after creation—users must use the refresh endpoint if the secret is lost +- **No Expiration Notifications**: Expired tokens are silently rejected; automatic expiration warnings are not implemented +- **Legacy Token Migration**: Existing CLI tokens are auto-migrated on first UI load but can be manually managed; no background rotation + +## Potential Future Enhancements + +- **Token Expiration Warnings**: Notify users when PATs are approaching expiration +- **Finer-Grained Scopes**: Expand scope support to per-repository level and per-action permissions (e.g., `pull` vs `push` vs `delete`) +- **Rate Limiting Configuration**: Operator-configurable rate limits specific to PAT requests +- **Default Expiration Policies**: System-wide or role-based policies enforcing token expiration requirements +- **Automatic Token Rotation**: Scheduled rotation strategies with optional notifications +- **Token Usage Analytics**: Dashboard showing token usage patterns, inactive tokens, and security insights +- **Revocation Callbacks**: Integration with CI/CD systems for automatic token invalidation on deployment events +- **OAuth 2.0 Support**: Native OAuth 2.0 token endpoint for standardized integration + +## References + +- **PR**: https://github.com/goharbor/harbor/pull/23370 +- **Code**: `/src/server/middleware/security/pat.go`, `/src/core/service/token/`, `/src/pkg/pat/` +- **Tests**: `/tests/robot-cases/Group1-Nightly/PAT.robot` +- Related Work: + - Docker Registry V2 Token Authentication + - GitHub Personal Access Tokens + - GitLab Personal Access Tokens + - Harbor Robot Account Implementation (`/src/pkg/robot/`) + - Harbor OIDC Authentication (linked feature for auto-onboarding) From dbe08ba6a315c3de5809a0a90d562c08c6410327 Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Mon, 29 Jun 2026 16:06:10 +0700 Subject: [PATCH 2/5] Add Probe-Based Authentication Dispatch proposal Proposes refactoring Harbor's authentication system from single global auth_mode to flexible per-backend matching. Fixes critical issues: - Admin lockout when OIDC provider is unavailable - Security vulnerability: locked users authenticate as nil - Registry proxy bypasses per-user permissions - HTTP Auth Proxy mode non-functional Enables fallback authentication paths while maintaining backward compatibility. PR: https://github.com/goharbor/harbor/pull/23458 Signed-off-by: Ross Golder --- proposals/probe-based-auth-dispatch.md | 403 +++++++++++++++++++++++++ 1 file changed, 403 insertions(+) create mode 100644 proposals/probe-based-auth-dispatch.md diff --git a/proposals/probe-based-auth-dispatch.md b/proposals/probe-based-auth-dispatch.md new file mode 100644 index 00000000..64351966 --- /dev/null +++ b/proposals/probe-based-auth-dispatch.md @@ -0,0 +1,403 @@ +# Proposal: Probe-Based Authentication Dispatch + +Author: Harbor Community + +**Status**: IN REVIEW +**Implementation PR**: https://github.com/goharbor/harbor/pull/23458 (Open) + +## Abstract + +Probe-based authentication dispatch replaces Harbor's single-backend `auth_mode` configuration with a flexible per-backend matching system. This fixes critical production issues: admin lockout when OIDC is unavailable, security vulnerability with locked users authenticating as nil, and registry proxy bypassing per-user permission enforcement. The solution enables fallback authentication paths, corrects HTTP Auth Proxy support, and maintains backward compatibility with existing deployments. + +## Background + +Harbor's authentication has historically relied on a global `auth_mode` configuration that rigidly selects a single authentication backend: + +``` +auth_mode = "db" # Database only +auth_mode = "ldap" # LDAP only +auth_mode = "oidc" # OIDC only +auth_mode = "http_auth" # HTTP Auth Proxy only +auth_mode = "uaa" # UAA only +``` + +### Limitations + +**1. Admin Lockout in OIDC Mode** +- OIDC provider becomes unavailable (misconfigured, IdP outage, network issue) +- No fallback to local database authentication +- Admin account becomes completely inaccessible +- Recovery requires Harbor restart and manual config edit + +**2. Security Bug: Locked Users Authenticate as nil** +- User account locked due to repeated login failures +- `Login()` returns `(nil, nil)` instead of error +- HTTP handlers treat nil as successful login with no user +- Locked accounts silently authenticate and pass authorization checks +- Permission enforcement is bypassed for locked accounts + +**3. Registry Proxy Ignores User Credentials** +- Registry proxy always forwards shared service-account credential to upstream +- Per-user permissions are completely bypassed +- Read-only user can push images (proxied as service account) +- No way to enforce repository-level access control + +**4. HTTP Auth Proxy Mode Silently Broken** +- `http_auth` mode missing from authentication dispatch switch +- All authentication falls through to database only +- HTTP Auth Proxy configuration non-functional despite config support +- Users cannot implement external authentication proxy pattern + +### Design Issues + +The root cause: single global `auth_mode` creates an inflexible all-or-nothing dispatch. Enterprise environments often need: +- OIDC as primary + database fallback (for admin access when IdP is down) +- Multiple auth backends with explicit fallback chains +- Per-user credential passthrough for registry proxy use cases +- HTTP Auth Proxy as first-class authentication method + +## Proposal + +### Core Changes + +#### 1. Per-Backend Match() Interface ✅ + +Each authentication backend implements `Match()` to report whether it should be tried: + +```go +type Authenticator interface { + // Login authenticates user with given credentials + Login(ctx context.Context, m *models.AuthModel) (*models.User, error) + + // Match reports if this backend is configured and should be tried + // Returns (true, nil) if configured; (false, nil) if not; (false, err) if misconfigured + Match(ctx context.Context) (bool, error) + + // OnBoardUser provisions new user if backend supports it + OnBoardUser(ctx context.Context, u *models.User) error + + // PostAuthenticate post-processes authenticated user (groups, attributes) + PostAuthenticate(ctx context.Context, u *models.User) error +} +``` + +#### 2. Multi-Backend Dispatch Logic ✅ + +The authenticator dispatcher tries backends in priority order; first success wins: + +``` +Try backends in priority order: +1. OIDC (if configured) +2. LDAP (if configured) +3. UAA (if configured) +4. HTTP Auth Proxy (if configured) +5. Database (always available as fallback) +6. Robot accounts (always via database) +7. Superuser (always via database) + +First successful authentication wins. +Errors from non-matching backends are ignored. +Errors from matching backends are fatal. +``` + +#### 3. Locked User Security Fix ✅ + +`Login()` now returns `error` for locked users instead of `(nil, nil)`: + +```go +// Before: Login returned (nil, nil) for locked users +user, err := authenticator.Login(ctx, model) +if user == nil && err == nil { // WRONG: treated as successful nil-user login + // allowed locked user to pass through +} + +// After: Login returns ErrAuth for locked users +user, err := authenticator.Login(ctx, model) +if err == ErrAuth { // Correct: explicitly failed auth + // reject locked user + return http.StatusUnauthorized +} +``` + +#### 4. Registry Proxy User Credential Passthrough ✅ + +Registry proxy now: +- **Probes** upstream registry to detect auth method (Bearer or Basic) +- **Exchanges** user credentials for scoped Bearer token via token service +- **Caches** tokens per-repository scope (not globally) +- **Falls back** to probed credentials for unauthenticated requests +- **Handles** token service failures gracefully + +**Token Caching Strategy:** +- Key: `:` (e.g., `user123:library/myapp`) +- Storage: In-memory with TTL matching token expiration +- Isolation: Separate token per repository scope, preventing privilege escalation +- Fallback: If caching unavailable, exchanges token on each request + +#### 5. Auth Probe Detection ✅ + +Registry proxy probes upstream to detect which auth method to use: + +``` +GET /v2/ with no credentials +-> If returns 401 with Bearer realm: + -> Use token service exchange for Bearer tokens +-> If returns 401 with Basic realm: + -> Use Basic auth passthrough +-> If returns 200: + -> Anonymous access supported +``` + +#### 6. HTTP Auth Proxy Mode Support ✅ + +`http_auth` mode now fully functional: +- HTTP Auth Proxy backend checked in dispatch order +- External auth endpoint validated via `Match()` +- User credentials forwarded to auth proxy +- Local user created on first successful auth (via `OnBoardUser()`) + +### Authentication Dispatch Flows + +#### OIDC Mode (with fallback) +``` +user login + → try OIDC + ✓ OIDC succeeds → return user + ✗ OIDC fails → continue + → try Database + ✓ DB succeeds → return user (admin always accessible) + ✗ DB fails → reject login +``` + +**Scenario**: OIDC provider down, admin needs access +- Admin logs in with database credentials +- OIDC probe fails, database login succeeds +- Admin regains access without config edit + +#### LDAP Mode (no fallback, fails clearly) +``` +user login + → try LDAP + ✓ LDAP succeeds → return user + ✗ LDAP fails → reject (clear error message) + (Database skipped; LDAP is primary) +``` + +#### HTTP Auth Proxy Mode +``` +user login with credentials + → try HTTP Auth Proxy + ✓ Proxy accepts → create/update local user, return + ✗ Proxy rejects → reject (clear error message) + (no other backends tried) +``` + +#### Database Mode +``` +user login + → try Database only + ✓ DB succeeds → return user + ✗ DB fails → reject +``` + +### Registry Proxy Auth + +#### Probe-based Passthrough +``` +Docker push myapp:latest +(user: alice, password: secret) + → probe upstream for auth method + returns: 401 Unauthorized, WWW-Authenticate: Bearer realm="..." + → exchange alice:secret with token service for Bearer token + → cache token with scope "myapp" for user alice + → forward image layers with Bearer token +``` + +#### Per-Scope Token Caching +``` +User alice pushes to: + 1. library/myapp → request Bearer token for "library/myapp" scope + → cache as alice:library/myapp + 2. library/otherapp → request Bearer token for "library/otherapp" scope + → cache as alice:library/otherapp (separate token) + 3. library/myapp again → reuse cached alice:library/myapp token + +Prevents privilege escalation via token reuse across repos. +``` + +### Configuration + +No configuration changes required. Existing `auth_mode` values work: + +| auth_mode | Behavior | Change from v2.15 | +|-----------|----------|-------------------| +| `db` | Database auth only | No change | +| `ldap` | LDAP auth only | No change | +| `oidc` | OIDC + DB fallback | **New**: fallback added (was OIDC only) | +| `http_auth` | HTTP Auth Proxy | **Fix**: now functional (was broken) | +| `uaa` | UAA auth only | No change | + +## Implementation (IN REVIEW) + +### Components + +**Core Authentication (`src/core/auth/`)** +- `authenticator.go` — `Match()` interface method; multi-backend dispatch; locked-user fix +- `authproxy/auth.go` — HTTP Auth Proxy `Match()` implementation +- `db/db.go`, `ldap/ldap.go`, `oidc/oidc.go`, `uaa/uaa.go` — Per-backend `Match()` implementations + +**Registry Proxy (`src/server/registry/`)** +- `proxy.go` — Probe-based auth detection; credential passthrough; scope-keyed token cache +- `proxy_test.go` — 372 new lines of test coverage + +**V2 Auth Middleware (`src/server/middleware/v2auth/`)** +- `auth.go` — Correct Bearer challenge for Basic-auth clients; graceful token service error handling + +**Config & UI (`src/lib/config/`, `src/portal/`)** +- Removed unused `AUTH_MODE` UI strings from portal +- Removed context helpers `WithAuthMode` / `AuthModeFrom` from `src/lib/context.go` +- Updated i18n files to remove `AUTH_MODE` keys + +### Test Coverage + +**Unit Tests** +- Registry proxy auth probing (3 test cases) +- Token caching and expiry (2 test cases) +- Scope extraction from request paths (8 test cases) +- Basic auth exchange (1 test case) +- Bearer challenge generation (1 test case) + +**Integration Tests** +- Registry push/pull with passthrough auth +- LDAP authentication flow +- OIDC + database fallback +- HTTP Auth Proxy endpoint validation + +**Existing Tests** +- All existing auth tests continue to pass +- Backward compatibility verified + +### Commits + +PR organized into 16 commits across 3 logical units: + +**Unit 1 — Security fixes (4 commits)** +- Locked user returns `ErrAuth` instead of nil +- Nil pointer dereference guards + +**Unit 2 — Registry proxy (5 commits)** +- Probe-based auth detection +- User credential passthrough +- Token scope caching +- Test coverage + +**Unit 3 — Core refactor (7 commits)** +- Per-backend `Match()` dispatch +- Remove global auth_mode switch +- Portal UI updates +- Config cleanup + +## Backward Compatibility ✅ + +**Configuration**: No changes required. All existing `auth_mode` values continue to work. + +**Database**: No migrations. Existing `auth_mode` configuration persisted as-is. + +**API**: No changes to public authentication APIs. + +**Behavior Changes** (all improvements): +- OIDC mode now has fallback to database (admin always accessible) +- HTTP Auth Proxy mode now functional (was broken) +- Registry proxy now respects per-user credentials (was security issue) +- Locked users now properly rejected (was security issue) + +## Issues Resolved + +| Issue | Title | Status | +|-------|-------|--------| +| #1572 | Harbor project lack of brute force mechanism | **FIXED** — locked users now return ErrAuth | +| #13372 | Combine multiple authentication modes | **ADDRESSES** — achieves multi-mode via fallback dispatch | +| #21300 | Passthrough Authentication in Proxy Cache | **IMPROVES** — user credentials now passed to token service | +| #7965 | Support local user creation in http_auth mode | **FIXES** — http_auth mode now fully functional | +| #7964 | Config option for token login in http_auth mode | **FIXES** — http_auth mode now fully functional | +| #21853 | OIDC and LDAP at the same time as auth mode | **ADDRESSES** — OIDC mode now falls through to DB | + +## Non-Goals + +- OAuth 2.0 as authentication mode (separate enhancement) +- Automatic auth backend selection (requires explicit configuration) +- Role-based auth backend routing (out of scope for this phase) +- User migration between auth backends (handled separately) +- Authentication performance optimization (orthogonal concern) + +## Known Limitations + +- **Token cache in-memory only**: Distributed Harbor deployments will re-exchange tokens on each instance. Distributed cache support is a future enhancement. +- **Per-scope tokens only**: Repository-level granularity not yet supported; future work to add per-action (pull/push) scoping. +- **No auto-fallback config**: Operators must set `auth_mode = "oidc"` to get fallback behavior; no automatic mode selection. +- **Token service errors**: Registry proxy gracefully degrades to Basic challenge on token service failure; tokens are not cached in this case. + +## Potential Future Enhancements + +- **Distributed token cache**: Redis or Memcached-backed token storage for multi-instance deployments +- **Per-action scoping**: Fine-grained Bearer token scopes (pull vs push vs delete) +- **Scheduled token cleanup**: Automatic removal of expired tokens from cache +- **Auth backend metrics**: Counters for authentication success/failure rates per backend +- **Audit logging**: Detailed logging of auth backend probe results and fallback decisions +- **Role-based routing**: Different auth backends for different user roles +- **OAuth 2.0 support**: Native OAuth 2.0 authentication mode alongside existing backends + +## Testing Strategy + +### Unit Testing +- `Match()` implementation per backend (all paths tested) +- Locked user error handling +- Probe-based detection logic +- Token scope extraction and caching +- Bearer challenge generation + +### Integration Testing +- Multi-backend dispatch with actual auth backends +- Registry proxy with upstream probe +- Token exchange with token service +- Fallback behavior across all `auth_mode` values + +### E2E Testing (Robot Framework) +- Docker login with each `auth_mode` +- Registry push/pull with proxy and passthrough auth +- OIDC provider unavailability + admin fallback +- Locked user rejected from all auth paths +- HTTP Auth Proxy external endpoint integration + +### Regression Testing +- All existing auth tests continue to pass +- Existing `auth_mode` configurations work without changes +- Database schema compatible (no migrations) + +## Migration Path + +**Phase 1 (v2.x)** — This PR +- Implement probe-based dispatch and multi-backend `Match()` +- Registry proxy user credential passthrough +- Security fixes for locked users and nil authentication +- HTTP Auth Proxy support + +**Phase 2 (v2.x+1)** +- Add distributed token cache option +- Implement per-action (pull/push) token scoping +- Auth backend metrics and audit logging + +**Phase 3 (v3.x)** +- Consider removing legacy `auth_mode` config in favor of explicit backend configuration +- OAuth 2.0 authentication support + +## References + +- **PR**: https://github.com/goharbor/harbor/pull/23458 +- **Code**: `src/core/auth/`, `src/server/registry/`, `src/server/middleware/v2auth/` +- **Related Issues**: #1572, #13372, #21300, #7965, #7964, #21853 +- Related Implementations: + - Docker Registry V2 auth probe + - GitHub token scoping strategy + - GitLab HTTP Auth support + - Kubernetes bearer token handling From 8ebf4eca9a5a77b8644906e76c71eb3b023adccf Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Mon, 29 Jun 2026 16:14:16 +0700 Subject: [PATCH 3/5] Simplify probe-based auth dispatch proposal to follow community template - Use standard Discussion field linking to related GitHub issues - Remove non-standard Status/Implementation PR fields - Condense to template format while preserving implementation details - Move detailed technical specs to Implementation and References sections - Follow Harbor community proposal conventions Signed-off-by: Ross Golder --- proposals/probe-based-auth-dispatch.md | 449 +++++++++---------------- 1 file changed, 151 insertions(+), 298 deletions(-) diff --git a/proposals/probe-based-auth-dispatch.md b/proposals/probe-based-auth-dispatch.md index 64351966..4295bcae 100644 --- a/proposals/probe-based-auth-dispatch.md +++ b/proposals/probe-based-auth-dispatch.md @@ -2,402 +2,255 @@ Author: Harbor Community -**Status**: IN REVIEW -**Implementation PR**: https://github.com/goharbor/harbor/pull/23458 (Open) +Discussion: [goharbor/harbor#1572](https://github.com/goharbor/harbor/issues/1572), [goharbor/harbor#13372](https://github.com/goharbor/harbor/issues/13372), [goharbor/harbor#21300](https://github.com/goharbor/harbor/issues/21300), [goharbor/harbor#23458](https://github.com/goharbor/harbor/pull/23458) ## Abstract -Probe-based authentication dispatch replaces Harbor's single-backend `auth_mode` configuration with a flexible per-backend matching system. This fixes critical production issues: admin lockout when OIDC is unavailable, security vulnerability with locked users authenticating as nil, and registry proxy bypassing per-user permission enforcement. The solution enables fallback authentication paths, corrects HTTP Auth Proxy support, and maintains backward compatibility with existing deployments. +Replace Harbor's single-backend `auth_mode` configuration with a flexible per-backend matching system. This fixes three critical production issues: admin lockout when OIDC provider is unavailable, security vulnerability where locked users authenticate as nil, and registry proxy bypassing per-user permissions. Enables fallback authentication chains while maintaining full backward compatibility. ## Background -Harbor's authentication has historically relied on a global `auth_mode` configuration that rigidly selects a single authentication backend: +Harbor currently uses a global `auth_mode` setting to select a single authentication backend: ``` -auth_mode = "db" # Database only -auth_mode = "ldap" # LDAP only -auth_mode = "oidc" # OIDC only -auth_mode = "http_auth" # HTTP Auth Proxy only -auth_mode = "uaa" # UAA only +auth_mode = "db" # Database only +auth_mode = "ldap" # LDAP only +auth_mode = "oidc" # OIDC only +auth_mode = "http_auth" # HTTP Auth Proxy only +auth_mode = "uaa" # UAA only ``` -### Limitations +This design has caused multiple production issues: -**1. Admin Lockout in OIDC Mode** +### Issue 1: Admin Lockout in OIDC Mode - OIDC provider becomes unavailable (misconfigured, IdP outage, network issue) - No fallback to local database authentication - Admin account becomes completely inaccessible -- Recovery requires Harbor restart and manual config edit +- Recovery requires Harbor restart and manual config editing -**2. Security Bug: Locked Users Authenticate as nil** +### Issue 2: Security Bug — Locked Users Authenticate as nil - User account locked due to repeated login failures - `Login()` returns `(nil, nil)` instead of error - HTTP handlers treat nil as successful login with no user -- Locked accounts silently authenticate and pass authorization checks -- Permission enforcement is bypassed for locked accounts +- Locked accounts silently authenticate and bypass authorization checks -**3. Registry Proxy Ignores User Credentials** +### Issue 3: Registry Proxy Ignores User Credentials - Registry proxy always forwards shared service-account credential to upstream -- Per-user permissions are completely bypassed +- Per-user permissions completely bypassed - Read-only user can push images (proxied as service account) - No way to enforce repository-level access control -**4. HTTP Auth Proxy Mode Silently Broken** -- `http_auth` mode missing from authentication dispatch switch +### Issue 4: HTTP Auth Proxy Mode Silently Broken +- `http_auth` mode missing from authentication dispatch entirely - All authentication falls through to database only -- HTTP Auth Proxy configuration non-functional despite config support -- Users cannot implement external authentication proxy pattern - -### Design Issues - -The root cause: single global `auth_mode` creates an inflexible all-or-nothing dispatch. Enterprise environments often need: -- OIDC as primary + database fallback (for admin access when IdP is down) -- Multiple auth backends with explicit fallback chains -- Per-user credential passthrough for registry proxy use cases -- HTTP Auth Proxy as first-class authentication method +- HTTP Auth Proxy configuration non-functional ## Proposal -### Core Changes - -#### 1. Per-Backend Match() Interface ✅ - -Each authentication backend implements `Match()` to report whether it should be tried: - -```go -type Authenticator interface { - // Login authenticates user with given credentials - Login(ctx context.Context, m *models.AuthModel) (*models.User, error) - - // Match reports if this backend is configured and should be tried - // Returns (true, nil) if configured; (false, nil) if not; (false, err) if misconfigured - Match(ctx context.Context) (bool, error) - - // OnBoardUser provisions new user if backend supports it - OnBoardUser(ctx context.Context, u *models.User) error - - // PostAuthenticate post-processes authenticated user (groups, attributes) - PostAuthenticate(ctx context.Context, u *models.User) error -} -``` +### Core Solution -#### 2. Multi-Backend Dispatch Logic ✅ - -The authenticator dispatcher tries backends in priority order; first success wins: +Replace single-backend dispatch with per-backend `Match()` method. Each backend reports whether it's configured and should be tried. The dispatcher tries backends in priority order; first success wins. +**Per-backend Match() interface:** ``` -Try backends in priority order: -1. OIDC (if configured) -2. LDAP (if configured) -3. UAA (if configured) -4. HTTP Auth Proxy (if configured) -5. Database (always available as fallback) -6. Robot accounts (always via database) -7. Superuser (always via database) - -First successful authentication wins. -Errors from non-matching backends are ignored. -Errors from matching backends are fatal. +Match(ctx) → (bool, error) + - (true, nil) = backend configured, should be tried + - (false, nil) = backend not configured, skip + - (false, err) = backend misconfigured, fatal error ``` -#### 3. Locked User Security Fix ✅ +### Authentication Dispatch Flows -`Login()` now returns `error` for locked users instead of `(nil, nil)`: +**OIDC Mode** (with fallback): +``` +1. Try OIDC + ✓ Success → return user + ✗ Fail → continue +2. Try Database (fallback) + ✓ Success → return user (admin always accessible) + ✗ Fail → reject login +``` -```go -// Before: Login returned (nil, nil) for locked users -user, err := authenticator.Login(ctx, model) -if user == nil && err == nil { // WRONG: treated as successful nil-user login - // allowed locked user to pass through -} +**LDAP Mode** (no fallback): +``` +1. Try LDAP + ✓ Success → return user + ✗ Fail → reject (clear error) +``` -// After: Login returns ErrAuth for locked users -user, err := authenticator.Login(ctx, model) -if err == ErrAuth { // Correct: explicitly failed auth - // reject locked user - return http.StatusUnauthorized -} +**HTTP Auth Proxy Mode** (new support): +``` +1. Try HTTP Auth Proxy + ✓ Success → create/update local user, return + ✗ Fail → reject (clear error) ``` -#### 4. Registry Proxy User Credential Passthrough ✅ +**Database Mode** (unchanged): +``` +1. Try Database only + ✓ Success → return user + ✗ Fail → reject +``` -Registry proxy now: -- **Probes** upstream registry to detect auth method (Bearer or Basic) -- **Exchanges** user credentials for scoped Bearer token via token service -- **Caches** tokens per-repository scope (not globally) -- **Falls back** to probed credentials for unauthenticated requests -- **Handles** token service failures gracefully +### Security Fixes -**Token Caching Strategy:** -- Key: `:` (e.g., `user123:library/myapp`) -- Storage: In-memory with TTL matching token expiration -- Isolation: Separate token per repository scope, preventing privilege escalation -- Fallback: If caching unavailable, exchanges token on each request +**Locked User Fix:** +- `Login()` now returns `ErrAuth` for locked users instead of `(nil, nil)` +- Locked accounts properly rejected from all auth paths +- HTTP handlers can distinguish between auth failure and unauthenticated access -#### 5. Auth Probe Detection ✅ +**Registry Proxy User Credential Passthrough:** +- Probe upstream registry to detect auth method (Bearer or Basic) +- Exchange user credentials for scoped Bearer token via token service +- Cache tokens per-repository scope (not globally; prevents privilege escalation) +- Fall back to probed credentials for unauthenticated requests +- Handle token service failures gracefully -Registry proxy probes upstream to detect which auth method to use: +### Registry Proxy Auth Probe ``` GET /v2/ with no credentials --> If returns 401 with Bearer realm: - -> Use token service exchange for Bearer tokens --> If returns 401 with Basic realm: - -> Use Basic auth passthrough --> If returns 200: - -> Anonymous access supported + ↓ +If 401 + Bearer realm header: + → Use token service exchange for Bearer tokens +If 401 + Basic realm header: + → Use Basic auth passthrough +If 200 (no auth required): + → Allow anonymous access ``` -#### 6. HTTP Auth Proxy Mode Support ✅ - -`http_auth` mode now fully functional: -- HTTP Auth Proxy backend checked in dispatch order -- External auth endpoint validated via `Match()` -- User credentials forwarded to auth proxy -- Local user created on first successful auth (via `OnBoardUser()`) - -### Authentication Dispatch Flows - -#### OIDC Mode (with fallback) -``` -user login - → try OIDC - ✓ OIDC succeeds → return user - ✗ OIDC fails → continue - → try Database - ✓ DB succeeds → return user (admin always accessible) - ✗ DB fails → reject login -``` +## Non-Goals -**Scenario**: OIDC provider down, admin needs access -- Admin logs in with database credentials -- OIDC probe fails, database login succeeds -- Admin regains access without config edit +- OAuth 2.0 as authentication mode (separate enhancement) +- Automatic auth backend selection (requires explicit configuration) +- Role-based auth backend routing (out of scope) +- User migration between auth backends (handled separately) +- Authentication performance optimization (orthogonal) -#### LDAP Mode (no fallback, fails clearly) -``` -user login - → try LDAP - ✓ LDAP succeeds → return user - ✗ LDAP fails → reject (clear error message) - (Database skipped; LDAP is primary) -``` +## Rationale -#### HTTP Auth Proxy Mode -``` -user login with credentials - → try HTTP Auth Proxy - ✓ Proxy accepts → create/update local user, return - ✗ Proxy rejects → reject (clear error message) - (no other backends tried) -``` +**Why per-backend Match()?** Allows each backend to report readiness independently, enabling smart fallback without complex conditional logic. -#### Database Mode -``` -user login - → try Database only - ✓ DB succeeds → return user - ✗ DB fails → reject -``` +**Why OIDC + DB fallback?** Admin access is critical. If OIDC provider fails, ops need a way back in without restarting Harbor. -### Registry Proxy Auth +**Why probe-based registry auth?** Different registries use different auth methods. Probing detects the method automatically; no manual configuration needed. -#### Probe-based Passthrough -``` -Docker push myapp:latest -(user: alice, password: secret) - → probe upstream for auth method - returns: 401 Unauthorized, WWW-Authenticate: Bearer realm="..." - → exchange alice:secret with token service for Bearer token - → cache token with scope "myapp" for user alice - → forward image layers with Bearer token -``` +**Why per-scope token caching?** Prevents privilege escalation. A token scoped to `repo1` cannot be reused for `repo2`. -#### Per-Scope Token Caching -``` -User alice pushes to: - 1. library/myapp → request Bearer token for "library/myapp" scope - → cache as alice:library/myapp - 2. library/otherapp → request Bearer token for "library/otherapp" scope - → cache as alice:library/otherapp (separate token) - 3. library/myapp again → reuse cached alice:library/myapp token - -Prevents privilege escalation via token reuse across repos. -``` +**Why fix locked-user bug?** Nil authentication creates security gap. Locked accounts should be locked, not silently pass through. -### Configuration +## Compatibility -No configuration changes required. Existing `auth_mode` values work: +**No configuration changes required.** All existing `auth_mode` values work: -| auth_mode | Behavior | Change from v2.15 | -|-----------|----------|-------------------| +| auth_mode | Behavior | Change | +|-----------|----------|--------| | `db` | Database auth only | No change | | `ldap` | LDAP auth only | No change | -| `oidc` | OIDC + DB fallback | **New**: fallback added (was OIDC only) | +| `oidc` | OIDC + DB fallback | **New**: fallback added (was OIDC-only) | | `http_auth` | HTTP Auth Proxy | **Fix**: now functional (was broken) | | `uaa` | UAA auth only | No change | -## Implementation (IN REVIEW) +**Backward Compatibility:** +- No database migrations required +- No API changes +- Existing authentication methods continue working +- Existing robot accounts continue working +- Configuration files need no updates -### Components +## Implementation -**Core Authentication (`src/core/auth/`)** -- `authenticator.go` — `Match()` interface method; multi-backend dispatch; locked-user fix -- `authproxy/auth.go` — HTTP Auth Proxy `Match()` implementation -- `db/db.go`, `ldap/ldap.go`, `oidc/oidc.go`, `uaa/uaa.go` — Per-backend `Match()` implementations +### Code Changes -**Registry Proxy (`src/server/registry/`)** -- `proxy.go` — Probe-based auth detection; credential passthrough; scope-keyed token cache -- `proxy_test.go` — 372 new lines of test coverage +**Core Authentication** (`src/core/auth/`): +- `authenticator.go` — New `Match()` interface; multi-backend dispatch; locked-user security fix +- `authproxy/auth.go`, `db/db.go`, `ldap/ldap.go`, `oidc/oidc.go`, `uaa/uaa.go` — Per-backend `Match()` implementations -**V2 Auth Middleware (`src/server/middleware/v2auth/`)** +**Registry Proxy** (`src/server/registry/`): +- `proxy.go` — Probe-based auth detection; credential passthrough; per-scope token caching +- `proxy_test.go` — 372 lines of test coverage + +**V2 Auth Middleware** (`src/server/middleware/v2auth/`): - `auth.go` — Correct Bearer challenge for Basic-auth clients; graceful token service error handling -**Config & UI (`src/lib/config/`, `src/portal/`)** -- Removed unused `AUTH_MODE` UI strings from portal -- Removed context helpers `WithAuthMode` / `AuthModeFrom` from `src/lib/context.go` -- Updated i18n files to remove `AUTH_MODE` keys +**Config & UI** (`src/lib/config/`, `src/portal/`): +- Remove unused `AUTH_MODE` UI strings +- Remove context helpers `WithAuthMode` / `AuthModeFrom` +- Update i18n files -### Test Coverage +### Testing -**Unit Tests** -- Registry proxy auth probing (3 test cases) -- Token caching and expiry (2 test cases) -- Scope extraction from request paths (8 test cases) -- Basic auth exchange (1 test case) -- Bearer challenge generation (1 test case) +**Unit Tests:** +- Registry proxy auth probing (3 cases) +- Token caching and expiry (2 cases) +- Scope extraction from request paths (8 cases) +- Basic auth exchange (1 case) +- Bearer challenge generation (1 case) -**Integration Tests** -- Registry push/pull with passthrough auth -- LDAP authentication flow -- OIDC + database fallback -- HTTP Auth Proxy endpoint validation +**Integration Tests:** +- Multi-backend dispatch with actual backends +- Registry proxy with upstream probe +- Token exchange with token service +- Fallback behavior across all `auth_mode` values -**Existing Tests** -- All existing auth tests continue to pass -- Backward compatibility verified +**E2E Tests:** +- Docker login with each `auth_mode` +- Registry push/pull with proxy and passthrough auth +- OIDC provider unavailability + admin fallback +- Locked user rejected from all paths +- HTTP Auth Proxy external endpoint integration ### Commits PR organized into 16 commits across 3 logical units: -**Unit 1 — Security fixes (4 commits)** +**Unit 1 — Security fixes** (4 commits) - Locked user returns `ErrAuth` instead of nil - Nil pointer dereference guards -**Unit 2 — Registry proxy (5 commits)** +**Unit 2 — Registry proxy** (5 commits) - Probe-based auth detection - User credential passthrough -- Token scope caching +- Per-scope token caching - Test coverage -**Unit 3 — Core refactor (7 commits)** +**Unit 3 — Core refactor** (7 commits) - Per-backend `Match()` dispatch - Remove global auth_mode switch - Portal UI updates - Config cleanup -## Backward Compatibility ✅ - -**Configuration**: No changes required. All existing `auth_mode` values continue to work. +## Known Limitations -**Database**: No migrations. Existing `auth_mode` configuration persisted as-is. +- **Token cache in-memory only**: Distributed deployments re-exchange tokens per instance; future: distributed cache support +- **Per-scope tokens only**: Repository-level granularity not yet supported; future work for per-action (pull/push/delete) scoping +- **No auto-fallback config**: Operators must explicitly set `auth_mode = "oidc"` for fallback; no automatic selection +- **Token service errors**: Registry proxy degrades to Basic challenge on token service failure; tokens not cached -**API**: No changes to public authentication APIs. +## Potential Future Enhancements -**Behavior Changes** (all improvements): -- OIDC mode now has fallback to database (admin always accessible) -- HTTP Auth Proxy mode now functional (was broken) -- Registry proxy now respects per-user credentials (was security issue) -- Locked users now properly rejected (was security issue) +- Distributed token cache (Redis/Memcached) for multi-instance Harbor +- Per-action token scoping (pull vs push vs delete) +- Scheduled token cleanup from cache +- Auth backend success/failure metrics +- Detailed audit logging of auth backend decisions +- Role-based auth backend routing +- OAuth 2.0 support as native authentication mode ## Issues Resolved -| Issue | Title | Status | -|-------|-------|--------| -| #1572 | Harbor project lack of brute force mechanism | **FIXED** — locked users now return ErrAuth | -| #13372 | Combine multiple authentication modes | **ADDRESSES** — achieves multi-mode via fallback dispatch | +| Issue | Title | Resolution | +|-------|-------|-----------| +| #1572 | Lack of brute force / account locking mechanism | **FIXED** — locked users now return ErrAuth | +| #13372 | Combine multiple authentication modes | **ADDRESSES** — multi-mode support via fallback dispatch | | #21300 | Passthrough Authentication in Proxy Cache | **IMPROVES** — user credentials now passed to token service | | #7965 | Support local user creation in http_auth mode | **FIXES** — http_auth mode now fully functional | | #7964 | Config option for token login in http_auth mode | **FIXES** — http_auth mode now fully functional | -| #21853 | OIDC and LDAP at the same time as auth mode | **ADDRESSES** — OIDC mode now falls through to DB | - -## Non-Goals - -- OAuth 2.0 as authentication mode (separate enhancement) -- Automatic auth backend selection (requires explicit configuration) -- Role-based auth backend routing (out of scope for this phase) -- User migration between auth backends (handled separately) -- Authentication performance optimization (orthogonal concern) - -## Known Limitations - -- **Token cache in-memory only**: Distributed Harbor deployments will re-exchange tokens on each instance. Distributed cache support is a future enhancement. -- **Per-scope tokens only**: Repository-level granularity not yet supported; future work to add per-action (pull/push) scoping. -- **No auto-fallback config**: Operators must set `auth_mode = "oidc"` to get fallback behavior; no automatic mode selection. -- **Token service errors**: Registry proxy gracefully degrades to Basic challenge on token service failure; tokens are not cached in this case. - -## Potential Future Enhancements - -- **Distributed token cache**: Redis or Memcached-backed token storage for multi-instance deployments -- **Per-action scoping**: Fine-grained Bearer token scopes (pull vs push vs delete) -- **Scheduled token cleanup**: Automatic removal of expired tokens from cache -- **Auth backend metrics**: Counters for authentication success/failure rates per backend -- **Audit logging**: Detailed logging of auth backend probe results and fallback decisions -- **Role-based routing**: Different auth backends for different user roles -- **OAuth 2.0 support**: Native OAuth 2.0 authentication mode alongside existing backends - -## Testing Strategy - -### Unit Testing -- `Match()` implementation per backend (all paths tested) -- Locked user error handling -- Probe-based detection logic -- Token scope extraction and caching -- Bearer challenge generation - -### Integration Testing -- Multi-backend dispatch with actual auth backends -- Registry proxy with upstream probe -- Token exchange with token service -- Fallback behavior across all `auth_mode` values - -### E2E Testing (Robot Framework) -- Docker login with each `auth_mode` -- Registry push/pull with proxy and passthrough auth -- OIDC provider unavailability + admin fallback -- Locked user rejected from all auth paths -- HTTP Auth Proxy external endpoint integration - -### Regression Testing -- All existing auth tests continue to pass -- Existing `auth_mode` configurations work without changes -- Database schema compatible (no migrations) - -## Migration Path - -**Phase 1 (v2.x)** — This PR -- Implement probe-based dispatch and multi-backend `Match()` -- Registry proxy user credential passthrough -- Security fixes for locked users and nil authentication -- HTTP Auth Proxy support - -**Phase 2 (v2.x+1)** -- Add distributed token cache option -- Implement per-action (pull/push) token scoping -- Auth backend metrics and audit logging - -**Phase 3 (v3.x)** -- Consider removing legacy `auth_mode` config in favor of explicit backend configuration -- OAuth 2.0 authentication support +| #21853 | OIDC and LDAP at the same time as auth mode | **ADDRESSES** — OIDC + DB fallback achieves some co-existence | ## References -- **PR**: https://github.com/goharbor/harbor/pull/23458 -- **Code**: `src/core/auth/`, `src/server/registry/`, `src/server/middleware/v2auth/` +- **Implementation**: [goharbor/harbor#23458](https://github.com/goharbor/harbor/pull/23458) - **Related Issues**: #1572, #13372, #21300, #7965, #7964, #21853 -- Related Implementations: - - Docker Registry V2 auth probe - - GitHub token scoping strategy - - GitLab HTTP Auth support - - Kubernetes bearer token handling +- **Code locations**: `src/core/auth/`, `src/server/registry/`, `src/server/middleware/v2auth/` +- **Related work**: Docker Registry V2 auth probe, Kubernetes bearer token handling, GitHub OAuth fallback chains From 111d8f2ee94540962442732274f7847c6d606d82 Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Mon, 29 Jun 2026 16:23:23 +0700 Subject: [PATCH 4/5] Remove personal-access-tokens.md from probe-based-auth-dispatch branch This file belongs on the personal-access-tokens branch only. The probe-based-auth-dispatch branch should contain only auth dispatch proposal changes. Signed-off-by: Ross Golder --- proposals/personal-access-tokens.md | 342 ---------------------------- 1 file changed, 342 deletions(-) delete mode 100644 proposals/personal-access-tokens.md diff --git a/proposals/personal-access-tokens.md b/proposals/personal-access-tokens.md deleted file mode 100644 index cee4b9e9..00000000 --- a/proposals/personal-access-tokens.md +++ /dev/null @@ -1,342 +0,0 @@ -# Proposal: Personal Access Tokens (PAT) - -Author: Ross Golder - -**Status**: COMPLETED -**Implementation PR**: https://github.com/goharbor/harbor/pull/23370 - -## Abstract - -Personal Access Tokens (PAT) provide a modern, scoped authentication mechanism for Harbor that enables secure CI/CD integration, programmatic access, and improved secret management. PATs represent a modernization of Harbor's existing CLI token system with enhanced lifecycle management, self-service capabilities, and comprehensive audit trails. - -## Background - -Harbor previously supported: -1. **User credentials** (username/password) for both UI and registry access -2. **Legacy CLI tokens** - proprietary tokens for programmatic access, created by admins -3. **Robot accounts** with project-level scope (admin-managed) - -Limitations of the legacy CLI token system: -- No expiration dates or time-based lifecycle management -- Limited self-service capabilities (admins controlled creation) -- No usage tracking or audit trails -- Inconsistent authentication mechanisms across registry and API -- No fine-grained access control beyond username level - -PATs modernize this approach by providing: -- Self-service token management for individual users -- Time-limited credentials with configurable expiration -- Usage tracking through `last_used_at` timestamps -- Consistent authentication across registry and API -- Automatic migration of legacy CLI tokens with backward compatibility -- Clear token prefix (`hbr_pat_`) for identification in logs and audit trails - -## Proposal - -### Core Features (COMPLETED) - -#### 1. PAT Lifecycle Management ✅ -Users can create, manage, and revoke their own PATs with: -- **Name**: User-defined identifier for the token -- **Description**: Optional documentation field -- **Expiration**: Configurable number of days (or -1 for never expire) -- **Creation Time**: Automatic timestamp -- **Update Time**: Automatic timestamp -- **Last Used At**: Tracks most recent authentication attempt -- **Disabled**: Flag to revoke access without deletion -- **Is Legacy**: Flag tracking tokens migrated from CLI secrets - -#### 2. Token Prefix System ✅ -- New PATs use prefix: `hbr_pat_` followed by the token secret -- Example: `hbr_pat_rKgjKEMpMEK23zqejkWn5GIVvgJps1vKACTa6tnGXXyOlOTsXFESccDvgaJx047q` -- Distinguishes from robot accounts (prefixed with `robot$`) -- Enables easy identification in logs and audit trails -- Legacy CLI tokens retain `is_legacy` flag for backward compatibility - -#### 3. Authentication Flow ✅ -PATs authenticate via HTTP Basic Auth (username + PAT secret): -- Registry authentication for Docker login and push/pull -- API endpoint access for programmatic operations -- Security middleware (`src/server/middleware/security/pat.go`): - 1. Checks for `hbr_pat_` prefix in credentials - 2. Looks up user by username - 3. Queries all active, non-legacy PATs for the user - 4. Validates expiration (token with future/unlimited expiration accepted) - 5. Verifies secret hash using SHA256 with per-token salt - 6. Updates `last_used_at` timestamp on successful match - 7. Returns security context with token scope - -#### 4. Scope Enforcement ✅ -- PATs include a scope field containing project-level permissions -- Scope structure: JSON with resource names and allowed actions -- Scope validation enforced through existing authorization layer (RAM) -- Restricts token access to permitted projects only - -#### 5. REST API Endpoints ✅ -``` -POST /api/v2.0/users/{user_id}/personal_access_tokens -GET /api/v2.0/users/{user_id}/personal_access_tokens -GET /api/v2.0/users/{user_id}/personal_access_tokens/{token_id} -PATCH /api/v2.0/users/{user_id}/personal_access_tokens/{token_id} -DELETE /api/v2.0/users/{user_id}/personal_access_tokens/{token_id} -POST /api/v2.0/users/{user_id}/personal_access_tokens/{token_id}/refresh -``` - -#### 6. Security Properties ✅ -- Secrets hashed using SHA256 with individual per-token salt values -- Token secrets never stored in plaintext in database -- Token secret returned only on creation (cannot be retrieved later) -- Disabled tokens rejected during authentication -- Expired tokens rejected during authentication -- Token scope validated during authorization checks - -#### 7. Audit Trail Integration ✅ -All PAT operations logged to Harbor's audit system: -- **Create**: User ID, token name, description, expiration date -- **Read**: User ID, token access queries -- **Update**: User ID, token modifications (enable/disable, refresh) -- **Delete**: User ID, token deletion -- **Usage**: Authentication attempts (success/failure) tracked via `last_used_at` -- Audit logs queryable via `/api/v2.0/audit-logs` with resource type filtering -- Compliance-ready: Includes user context, timestamps, and operation details - -### Database Schema (COMPLETED) - -```sql -CREATE TABLE personal_access_token ( - id SERIAL PRIMARY KEY, - user_id INT NOT NULL REFERENCES harbor_user(user_id), - name VARCHAR(255) NOT NULL, - description TEXT, - secret VARCHAR(7168) NOT NULL, -- SHA256 hash with salt - salt VARCHAR(255) NOT NULL, - expires_at BIGINT DEFAULT -1, -- Unix timestamp, -1 = never expire - creation_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - update_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP, - last_used_at BIGINT, - disabled BOOLEAN DEFAULT false, - is_legacy BOOLEAN DEFAULT false, - scope JSONB, - UNIQUE(user_id, name) -); - -CREATE INDEX idx_pat_user_id ON personal_access_token(user_id); -CREATE INDEX idx_pat_disabled ON personal_access_token(disabled); -CREATE INDEX idx_pat_expires_at ON personal_access_token(expires_at); -``` - -## Non-Goals - -- LDAP/OIDC provider-specific integration (handled separately) -- PAT login to Harbor UI (tokens are API-only, UI uses session auth) -- Hierarchical scopes beyond project level (future enhancement) -- Automatic expiration notifications or reminders (operational concern) -- Built-in rate limiting per PAT (can use reverse proxy) -- Token versioning or rotation strategies beyond manual refresh - -## Implementation (COMPLETED) ✅ - -### Components Implemented - -1. **Data Models** ✅ - - `/src/common/models/personal_access_token.go` - swagger-generated model - - `/src/server/v2.0/models/personal_access_token*.go` - API models - -2. **Database Layer** ✅ - - `/src/pkg/pat/dao/` - CRUD operations for token lifecycle - - `/src/pkg/pat/model/` - PAT model with hashing and validation - -3. **Service Layer** ✅ - - `/src/core/service/token/token.go` - Core token service - - `/src/core/service/token/token_test.go` - Unit tests - -4. **API Handlers** ✅ - - `/src/server/v2.0/handlers/userdata.go` - REST endpoint handlers - - Integrates with user controller for token CRUD - -5. **Security Middleware** ✅ - - `/src/server/middleware/security/pat.go` - PAT authentication handler - - `/src/server/middleware/security/pat_test.go` - Authentication tests - - Plugs into security context chain - -6. **Controller Layer** ✅ - - `/src/controller/pat/` - PAT business logic controller - - Handles token creation, validation, refresh - -7. **API Models** ✅ - - `PersonalAccessToken` - token representation - - `PersonalAccessTokenCreateRequest` - creation payload - - `PersonalAccessTokenCreatedResponse` - response with secret - - `PersonalAccessTokenRefreshRequest` - refresh payload - - `PersonalAccessTokenUpdateRequest` - patch operations - -### Test Coverage (COMPLETED) ✅ - -#### Unit Tests -- `/src/core/service/token/token_test.go` - Token service logic -- `/src/server/middleware/security/pat_test.go` - Authentication middleware -- `/src/pkg/pat/dao/` - Database layer operations -- Secret hashing and verification with salt -- Expiration validation logic -- Scope parsing and enforcement - -#### Integration Tests -- API endpoint CRUD operations -- Database persistence and transactions -- User lookup and token association -- Authentication middleware chain integration - -#### E2E Tests (Robot Framework) ✅ -Located in `/tests/robot-cases/Group1-Nightly/PAT.robot`: - -| Test Case | Status | -|-----------|--------| -| Admin creates PAT with expiry | ✅ PASSED | -| PAT list shows creation/expiration dates | ✅ PASSED | -| Refresh PAT secret | ✅ PASSED | -| Enable and disable PAT | ✅ PASSED | -| Delete PAT | ✅ PASSED | -| Non-admin user creates own PAT | ✅ PASSED | -| PAT with never-expire setting | ✅ PASSED | -| Docker login and push with PAT | ✅ PASSED | -| Expired PAT rejected for authentication | ✅ PASSED | -| Disabled PAT rejected for authentication | ✅ PASSED | -| PAT scope enforcement (project access) | ✅ PASSED | -| OIDC auto-onboarding with email lookup | ✅ DOCUMENTED | - -### Release Notes - -**Release**: v2.16.0+ -**Feature**: Personal Access Tokens (PAT) - -#### User-Facing Changes -- Users can now create and manage personal access tokens via API (`/api/v2.0/users/{user_id}/personal_access_tokens`) -- Tokens support configurable expiration dates -- Tokens can be disabled/enabled without deletion -- Tokens track creation, update, and last-used timestamps -- Legacy CLI tokens automatically flagged as `is_legacy` for backward compatibility -- Docker login now supports PAT credentials using `hbr_pat_` prefix - -#### Operator Changes -- New database table: `personal_access_token` -- Database migrations auto-apply on startup -- No configuration changes required -- Existing authentication methods continue to work - -#### Breaking Changes -- None - fully backward compatible with existing credentials and robot accounts - -## Rationale - -### Design Choices - -**Prefix System (`hbr_pat_`)**: Simplifies middleware identification and prevents conflicts with robot accounts. Makes security logs and audit trails more readable at a glance. - -**User Self-Service**: Unlike legacy CLI tokens (admin-managed) and robot accounts (project-admin-managed), PATs enable individual developers to control their own authentication without intermediaries. This improves developer experience and reduces operational overhead. - -**Expiration Support**: Time-bound credentials reduce blast radius of token leaks. Default of never-expire (-1) maintains backward compatibility while allowing operators to enforce policies. - -**Scope in JSON**: Flexible structure supports future expansion of access control granularity (per-repository, per-action, etc.) without database schema changes. - -**Legacy Token Support**: Migrating existing CLI secrets to PATs with `is_legacy` flag preserves existing automation while enabling path to modern controls. - -**Last Used Tracking**: Enables security audits, cleanup of unused tokens, and compliance reporting without requiring external logging infrastructure. - -### Alternate Approaches Considered - -1. **OAuth 2.0 Token Endpoint**: More complex; PAT approach simpler for internal Harbor-only use -2. **Per-Project Token Limits**: Can be enforced through policy/operators, not required in core -3. **Automatic Token Rotation**: Manual refresh endpoint provides operator control; automatic rotation adds unnecessary complexity -4. **Unified Token Type**: Keeping legacy CLI tokens separate allows gradual migration without forcing all users to rotate credentials immediately - -## Compatibility - -### Backward Compatibility ✅ -- Existing user credentials (username/password) continue to work -- Existing robot accounts continue to work -- Existing legacy CLI tokens continue to work (marked with `is_legacy=true`) -- No changes required to client code -- Additive API endpoints only - -### Authentication Middleware Stack (Updated Order) -1. Secret/JWT token handler (Harbor internal) -2. **PAT handler** (new - checks for `hbr_pat_` prefix) -3. Robot handler (existing - checks for `robot$` prefix) -4. Basic Auth handler (existing - username/password) -5. Session handler (existing - cookie-based) -6. Unauthorized handler (fallback) - -Order ensures modern tokens checked first, legacy mechanisms still supported. - -### API Versioning -- Uses existing `/api/v2.0` endpoint versioning -- All endpoints are additive, no breaking changes -- Existing endpoints unaffected - -### Storage Considerations -- Single new table: `personal_access_token` -- No migration of existing user data required -- Indexes on: `user_id`, `disabled`, `expires_at` -- No impact on existing database size or performance - -## Testing Results - -### Test Execution Summary -- **Unit Tests**: All passing (token service, middleware, DAO) -- **Integration Tests**: All passing (API endpoints, database layer) -- **E2E Tests**: 12/12 test cases passing -- **Code Quality**: 0 lint issues - -### Test Coverage -- ✅ Token creation with various expiration configurations -- ✅ Secret hashing and verification with salt -- ✅ Expiration validation (expired tokens rejected) -- ✅ Disabled tokens rejected during authentication -- ✅ Scope parsing and enforcement -- ✅ Docker login authentication -- ✅ API CRUD operations -- ✅ Non-admin user self-service -- ✅ Last-used timestamp tracking -- ✅ Error handling for invalid inputs - -## Harbor UI Dashboard ✅ - -PAT management is fully integrated into the Harbor UI account settings: -- **PAT Management Tab** in account settings modal -- **Datagrid listing** showing name, creation date, expiration date, and status (enabled/disabled) -- **Create PAT modal** with fields: name, description, expiration (days or never) -- **Token secret display** in readonly field with copy-to-clipboard button (shown only on creation) -- **Manage operations**: enable/disable toggle per token, refresh secret, delete with confirmation -- **Auto-migration**: Existing CLI secrets automatically converted to PAT format on first load -- **i18n support**: All UI strings localized for multiple languages -- **Error handling**: Friendly 409 error message for duplicate token names - -## Known Limitations - -- **Project-Level Scope**: Tokens have project-level access control; per-repository or per-action granularity (pull vs push) would require schema expansion -- **No Secret Retrieval**: Token secrets cannot be retrieved after creation—users must use the refresh endpoint if the secret is lost -- **No Expiration Notifications**: Expired tokens are silently rejected; automatic expiration warnings are not implemented -- **Legacy Token Migration**: Existing CLI tokens are auto-migrated on first UI load but can be manually managed; no background rotation - -## Potential Future Enhancements - -- **Token Expiration Warnings**: Notify users when PATs are approaching expiration -- **Finer-Grained Scopes**: Expand scope support to per-repository level and per-action permissions (e.g., `pull` vs `push` vs `delete`) -- **Rate Limiting Configuration**: Operator-configurable rate limits specific to PAT requests -- **Default Expiration Policies**: System-wide or role-based policies enforcing token expiration requirements -- **Automatic Token Rotation**: Scheduled rotation strategies with optional notifications -- **Token Usage Analytics**: Dashboard showing token usage patterns, inactive tokens, and security insights -- **Revocation Callbacks**: Integration with CI/CD systems for automatic token invalidation on deployment events -- **OAuth 2.0 Support**: Native OAuth 2.0 token endpoint for standardized integration - -## References - -- **PR**: https://github.com/goharbor/harbor/pull/23370 -- **Code**: `/src/server/middleware/security/pat.go`, `/src/core/service/token/`, `/src/pkg/pat/` -- **Tests**: `/tests/robot-cases/Group1-Nightly/PAT.robot` -- Related Work: - - Docker Registry V2 Token Authentication - - GitHub Personal Access Tokens - - GitLab Personal Access Tokens - - Harbor Robot Account Implementation (`/src/pkg/robot/`) - - Harbor OIDC Authentication (linked feature for auto-onboarding) From 7b28e4ecaabfa043dc398dcaa791e2391490b12d Mon Sep 17 00:00:00 2001 From: Ross Golder Date: Mon, 29 Jun 2026 16:26:03 +0700 Subject: [PATCH 5/5] Clarify that commit organization describes Harbor PR, not proposal PR Signed-off-by: Ross Golder --- proposals/probe-based-auth-dispatch.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/proposals/probe-based-auth-dispatch.md b/proposals/probe-based-auth-dispatch.md index 4295bcae..18513d3a 100644 --- a/proposals/probe-based-auth-dispatch.md +++ b/proposals/probe-based-auth-dispatch.md @@ -200,9 +200,9 @@ If 200 (no auth required): - Locked user rejected from all paths - HTTP Auth Proxy external endpoint integration -### Commits +### Implementation PR Organization -PR organized into 16 commits across 3 logical units: +The Harbor implementation PR [#23458](https://github.com/goharbor/harbor/pull/23458) is organized into 16 commits across 3 logical units: **Unit 1 — Security fixes** (4 commits) - Locked user returns `ErrAuth` instead of nil @@ -220,6 +220,8 @@ PR organized into 16 commits across 3 logical units: - Portal UI updates - Config cleanup +These can be reviewed separately if preferred; Unit 2 (registry proxy) is independent of Unit 3. + ## Known Limitations - **Token cache in-memory only**: Distributed deployments re-exchange tokens per instance; future: distributed cache support