📝 Description
Persistent credentials issued to API keys and AI agents were handled as human credentials by the PostgreSQL, HTTP, and SSH proxies, which incorrectly required an IDP user token. This change preserves the non-human identity across the gateway internal gRPC hop. It also keeps PostgreSQL setup errors inside the negotiated TLS connection.
📣 User-facing impact
API keys and AI agents can use persistent PostgreSQL, HTTP, and SSH credentials. PostgreSQL TLS clients receive protocol-correct setup errors instead of SSL error: bad record type.
🔗 Related Issue
Self-contained reproductions:
- PostgreSQL AI-agent credential
- HTTP AI-agent credential
- SSH AI-agent credential
- PostgreSQL TLS error framing
🚀 Type of Change
- 🐛 Bug fix (non-breaking change which fixes an issue)
- ✨ New feature (non-breaking change which adds functionality)
- 💥 Breaking change (fix or feature that would cause existing functionality to not work as expected)
- 📚 Documentation update
- 🎨 Style/UI update
- ♻️ Code refactor
- ⚡ Performance improvement
- ✅ Test update
- 🔧 Build configuration change
- 🧹 Chore
📋 Changes Made
- recognize active machine identities, API keys, and AI agents when authenticating connection credentials
- preserve service identity context through the internal gRPC authentication path
- apply the shared behavior to PostgreSQL, HTTP, and SSH proxies
- write PostgreSQL setup failures through the post-negotiation connection
🧪 Testing
Test Configuration:
- Browser(s): Not applicable
- OS: Linux with Docker
Tests performed:
- Unit tests pass
- Integration tests pass
- Manual testing completed
The self-contained HTTP and SSH cases reproduce access token not found for user subject on Hoop 1.160.1 after their target preflight succeeds. The focused Go package command is blocked locally because github.com/hoophq/libhoop is private; repository CI has credentials for it.
📸 Screenshots (if applicable)
Not applicable.
✅ Checklist
- My code follows the style guidelines of this project
- I have performed a self-review of my own code
- I have commented my code, particularly in hard-to-understand areas
- My changes generate no new warnings
- New and existing unit tests pass locally with my changes
- I have checked my code and corrected any misspellings
📄 Additional Notes
Focused command blocked by private module access:
go test ./gateway/proxyproto/grpckey ./gateway/proxyproto/postgresproxy ./gateway/proxyproto/httpproxy ./gateway/proxyproto/sshproxy -count=1The PR remains draft until CI can validate the affected packages.
Activity
8I have read the CLA Document and I hereby sign the CLA
PR Summary by Qodo
Support persistent proxy credentials for service identities
🐞 Bug fix 🧪 Tests 🕐 40+ Minutes
AI Description
-
• Recognizes active machine, API-key, and AI-agent owners of persistent credentials. • Preserves service identity through PostgreSQL, HTTP, SSH, and internal gRPC authentication. • Sends PostgreSQL setup failures through negotiated TLS and adds focused regression coverage.
Diagram
-
sequenceDiagram actor Client participant Proxy as Protocol Proxies participant Models as Identity Models participant DB as Gateway Database participant Auth as gRPC Auth participant Stream as Gateway Stream Client->>Proxy: Present persistent credential Proxy->>Models: Classify credential owner Models->>DB: Query active identity DB-->>Models: Active owner match Models-->>Proxy: Service identity result Proxy->>Auth: Forward subject and org Auth->>Models: Load service context Models->>DB: Read identity and organization DB-->>Models: Active identity context Models-->>Auth: Authorized context Auth->>Stream: Open attributed session alt PostgreSQL setup fails after TLS Proxy-->>Client: TLS-framed ErrorResponse end
High-Level Assessment
-
The following are alternative approaches to this PR:
1. Typed credential-owner resolution
-
- ➕ Resolve human, machine, API-key, and AI-agent ownership through one model API.
- ➕ Avoid separate classification queries and protocol-specific branching.
- ➕ Reduce the chance of identity behavior drifting between proxies.
- ➖ Requires a broader credential model and proxy refactor.
- ➖ Increases regression risk for a targeted production authentication fix.
- ➖ May require migration or compatibility work for existing credential records.
Recommendation: Use the PR’s targeted classification and metadata propagation for this fix because it preserves existing machine and human authentication paths while correcting all affected proxies. Consider typed credential-owner resolution later to consolidate repeated classification logic and database lookups.
-
Files changed (8) +302 / -14
-
Bug fix (6)
+176 / -14-
machineidentity.go
Classify and load active service identities+59/-0
Classify and load active service identities
• Adds fail-closed credential classification across machine identities, API keys, and AI agents. Builds an organization-scoped context for active API-key or AI-agent identities.grpckey.go
Define service identity gRPC metadata keys+5/-0
Define service identity gRPC metadata keys
• Introduces metadata keys identifying service credentials and carrying their organization scope across internal gRPC calls.httpproxy.go
Authenticate HTTP service credentials without user tokens+13/-3
Authenticate HTTP service credentials without user tokens
• Classifies persistent HTTP credential owners, skips human IDP token verification and polling for service identities, and forwards service identity metadata to gRPC.postgresproxy.go
Support PostgreSQL service credentials and TLS-safe errors+44/-6
Support PostgreSQL service credentials and TLS-safe errors
• Applies service identity authentication and gRPC propagation to PostgreSQL credentials. Tracks the negotiated connection so setup failures are returned as PostgreSQL errors inside TLS rather than written to the raw socket.sshproxy.go
Preserve service identity through SSH authentication+22/-5
Preserve service identity through SSH authentication
• Stores service identity details in SSH permission extensions, bypasses human token checks for non-human credentials, and forwards the identity over internal gRPC.auth.go
Authenticate propagated service identities in gRPC+33/-0
Authenticate propagated service identities in gRPC
• Recognizes service identity metadata on impersonated streams, reconstructs the active API-key or AI-agent context, and resolves the requested connection under that identity.
Tests (2)
+126 / -0-
connection_credentials_test.go
Verify AI-agent credentials retain service identity+57/-0
Verify AI-agent credentials retain service identity
• Adds an integration test that creates an AI agent and PostgreSQL persistent credential, then verifies service-owner classification and context reconstruction.postgresproxy_test.go
Test PostgreSQL setup errors after TLS negotiation+69/-0
Test PostgreSQL setup errors after TLS negotiation
• Adds a pipe-based TLS regression test proving a post-handshake setup failure arrives as a PostgreSQL ErrorResponse instead of an invalid TLS record.
-
Code Review by Qodo
🐞 Bugs (0) 📘 Rule violations (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)
1. Packet tests drift from protocol types ✓ Resolved 📘 Rule violation ≡ Correctness
Description
TestConnectionSetupErrorStaysInsideTLS compares the parsed response type with the raw packet identifier "E" instead of the shared pgtypes.ServerErrorResponse constant. The assertion can diverge from the central protocol definition when packet handling is maintained or copied into additional tests.
Code
gateway/proxyproto/postgresproxy/postgresproxy_test.go[R63-64]+ if string(responseType) != "E" { + t.Fatalf("response type = %q, want PostgreSQL ErrorResponse E", responseType)
Relevance
●●● StrongThis is a deterministic protocol-constant/style correction, explicitly matching the repository rule.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
Rule 1800908 prohibits raw literals used as protocol packet identifiers. The added test parses a PostgreSQL response type and compares it to "E", while the repository already defines that identifier as pgtypes.ServerErrorResponse.
Rule 1800908: Use protocol packet type constants instead of raw string literalsgateway/proxyproto/postgresproxy/postgresproxy_test.go[59-64]common/pgtypes/const.go[39-39]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The PostgreSQL setup test identifies an error packet through the raw string literal `"E"`. ## Issue Context Use the existing shared PostgreSQL packet-type constant and compare the response byte without converting it to a string. ## Fix Focus Areas - gateway/proxyproto/postgresproxy/postgresproxy_test.go[59-64]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
2. Identity rules leak into data models ✓ Resolved 📘 Rule violation ⌂ Architecture
Description
GetServiceIdentityContextByID chooses between API-key and AI-agent identities, enforces active status, loads organization data, and assembles an authentication context inside gateway/models. Service-authentication policy now spans the model and interceptor layers, so later identity-policy changes can diverge instead of remaining in a gateway service.
Code
gateway/models/machineidentity.go[R266-268]+// GetServiceIdentityContextByID loads an active API key or AI agent context. +func GetServiceIdentityContextByID(orgID, id string) (*Context, error) { + apiKey, err := GetAPIKeyByNameOrID(orgID, id)
Relevance
●●● StrongRecent PR #1709 accepted moving business and cross-model workflow logic out of gateway/models.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
Rule 1800917 prohibits business workflows and cross-model coordination in gateway/models. The helper coordinates API-key, AI-agent, and organization lookups and applies the active-identity authentication rule before constructing the gateway context.
Rule 1800917: Keep business logic out of gateway/models and in gateway/servicesgateway/models/machineidentity.go[266-303]gateway/transport/interceptors/auth/auth.go[320-342]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description Service-identity authentication policy and context assembly were added to the data-model package. ## Issue Context Keep model code focused on persistence and move identity selection, active-status policy, organization loading, and context construction into a gateway service used by the interceptor. ## Fix Focus Areas - gateway/models/machineidentity.go[266-303] - gateway/transport/interceptors/auth/auth.go[320-342]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
3. Callers cannot control database access ✓ Resolved 📘 Rule violation ⌂ Architecture
Description
IsServiceIdentityCredential queries the package-global DB, and GetServiceIdentityContextByID also performs lookups without accepting a *gorm.DB. Callers cannot run these authentication queries on their transaction or injected test database, so proxy authentication may observe state outside the caller's database context.
Relevance
● WeakRecent PR #1649 rejected the same global-DB dependency-injection finding in gateway models.
ⓘ Recommendations generated based on similar findings in past PRs
Evidence
Rule 1800914 requires modified gateway database helpers to accept and use a *gorm.DB. The new credential classifier uses global DB directly, while the context helper invokes several database-backed model functions without receiving a database handle.
Rule 1800914: Gateway model functions must accept *gorm.DB instead of using the models.DB globalgateway/models/machineidentity.go[250-263]gateway/models/machineidentity.go[266-289]
Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution ## Issue description The new service-identity model helpers perform database access without accepting an injected `*gorm.DB`. ## Issue Context Update the helpers and their callers to pass and consistently use a database handle, allowing transactions and tests to control the database context. ## Fix Focus Areas - gateway/models/machineidentity.go[250-289] - gateway/proxyproto/httpproxy/httpproxy.go[446-449] - gateway/proxyproto/postgresproxy/postgresproxy.go[281-284] - gateway/proxyproto/sshproxy/sshproxy.go[267-270] - gateway/transport/interceptors/auth/auth.go[320-342]
ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools
Context sources
53 rules✅ Cross-repo context — repo relationships
repo: hoophq/libhoop (sha: 6d0fabb8)🧠 Deep: This security-sensitive, cross-cutting authentication change modifies multiple proxy paths, database identity classification, gRPC propagation, and PostgreSQL TLS error handling, creating several independent opportunities for subtle defects.Tip of the day
💡 Did you know, you can tweak Display preferences with a live preview to see your comment before it ships
All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.