uhc-auth-proxy is an authentication proxy that validates OpenShift operator requests against Red Hat's account management service and returns an identity JSON payload. It has exactly two functional endpoints and a narrow set of accepted clients.
| Method | Path | Handler | Purpose |
|---|---|---|---|
| GET | / |
RootHandler |
Primary auth endpoint |
| GET | /api/uhc-auth-proxy/v1 |
RootHandler |
Versioned alias of / |
| GET | /status |
StatusHandler |
Health/readiness check |
| ANY | /metrics |
promhttp.Handler |
Prometheus metrics |
The / and /api/uhc-auth-proxy/v1 routes are registered with r.Get and share the same handler variable. The /metrics route is registered with r.Handle, which accepts all HTTP methods, though Prometheus scrapers always use GET. Any new auth behavior added to the shared handler automatically applies to both auth routes.
Every request to the auth endpoint must include both headers. Missing or malformed values return 400.
Format: <operator-prefix><version> cluster/<cluster-id>
The operator prefix must be one of these exact strings (including the trailing slash):
insights-operator/cost-mgmt-operator/marketplace-operator/acm-operator/assisted-installer-operator/cryostat-operator/openshift-lightspeed-operator/jws-operator/runtimes-inventory-operator/
The second space-delimited segment must begin with cluster/. The cluster ID is everything after that prefix.
When adding a new operator, you must:
- Add a
constwith the prefix string inserver.goand add it to theoperatorPrefixesarray (update the array size literal). Note: existing constants use a*Prefixnaming convention (e.g.,insightsOperatorPrefix), exceptacmOperatorwhich does not follow this pattern. - Add the operator name without the trailing slash (e.g.,
"my-operator") to thevalidOperatorAgentsslice inserver_test.go.
Format: Bearer <token>
Must start with exactly Bearer (capital B, single space). The token value after the prefix must be non-empty or the request will fail with 500 (empty token creates an incomplete Registration).
The proxy reads x-rh-insights-request-id from the incoming request and uses request_id.ConfiguredRequestID middleware to generate/propagate it. Both the original header value and the middleware-generated ID are logged on every request.
Content-Type: application/json
{
"account_number": "<ebs_account_id>",
"org_id": "<external_id>",
"type": "System",
"internal": {
"org_id": "<external_id>"
},
"system": {
"cluster_id": "<cluster_id>"
}
}Key facts:
typeis always the literal string"System"— never vary this.account_numbercomes fromorganization.ebs_account_idin the upstream account.org_id(top-level) andinternal.org_idboth come fromorganization.external_id. Keep them in sync.system.cluster_idis echoed from the request's User-Agent, not from the upstream service.- Successful responses are cached for 2 hours keyed on
<cluster_id>:<token>. Cached responses are served as raw bytes, so they are byte-identical to the original.
Errors are returned as plain text (not JSON). Only success responses set Content-Type: application/json.
| Code | Condition | Body prefix |
|---|---|---|
| 400 | Invalid User-Agent | Invalid user-agent: |
| 400 | Invalid Authorization header | Invalid authorization header: |
| 500 | Empty token / incomplete Registration | Could not form valid cluster registration object: |
| 401 | Upstream auth failure without an HttpError (default) |
Could not authenticate: |
| upstream code | Upstream returns an HttpError (any 4xx/5xx code) |
Could not authenticate: |
| 500 | JSON marshal failure | Unable to read identity: |
Error status code logic (getErrorStatusCode):
- If the error wraps a
client.HttpError, use itsStatusCodefield directly, regardless of the value. - Otherwise, default to 401.
GET /status returns {"status": "available"} with no authentication required.
Applied in this order via chi.Use:
request_id.ConfiguredRequestID("x-rh-insights-request-id")— generates/propagates request IDsmiddleware.RealIP— trusts X-Forwarded-For / X-Real-IPmiddleware.Logger— request loggingmiddleware.Recoverer— panic recovery returns 500middleware.StripSlashes— trailing slashes normalized
Do not reorder these. The request-id middleware must be first.
- Cache key:
<cluster_id>:<bearer_token>(colon-separated) - TTL: 2 hours (hardcoded in
cache.Set) - Cache stores raw JSON bytes, not deserialized structs
- Cache is an in-memory
map[string]itemwith a global mutex (no distributed cache) - On cache hit, the upstream account management service is not called
cache.Clear()is used in tests between scenarios; there is no runtime cache invalidation endpoint
The HTTPWrapper.Do method adds headers for the upstream call to the account management service:
Authorization: AccessToken <cluster_id>:<authorization_token>
Accept: application/json
Content-Type: application/json
Note: the upstream Authorization format is AccessToken, not Bearer. This is a different scheme from what the proxy itself accepts.
All Prometheus metrics use the uhc_auth_proxy_ prefix:
| Metric | Type | Labels |
|---|---|---|
uhc_auth_proxy_cache_hit |
Counter | none |
uhc_auth_proxy_cache_miss |
Counter | none |
uhc_auth_proxy_responses |
Counter | code |
uhc_auth_proxy_request_time |
Histogram | url |
uhc_auth_proxy_to_acct_mgmt_request_status |
Counter | code |
When adding new metrics, follow this naming convention and always use promauto.New*.
- No request body parsing. The proxy authenticates purely from headers.
- Error responses are plain text. Do not return JSON error bodies from the auth handler.
- New operators require two code changes and one test change: add a const and array entry (updating the array size literal) in
server.go, and add the operator name (without trailing slash) tovalidOperatorAgentsinserver_test.go. - Status codes from upstream are forwarded. Do not mask or remap upstream 4xx/5xx codes wrapped in
client.HttpError. - Cache invalidation does not exist at runtime. The 2-hour TTL is the only mechanism.
- The
/and/api/uhc-auth-proxy/v1routes share the same handler variable and are therefore always in sync. Do not register separate handlers for these routes. - Content-Type is only set on success. Error paths in the auth handler intentionally omit it.