This proxy authenticates OpenShift cluster operators (insights-operator, cost-mgmt-operator, etc.) by validating their cluster_id and authorization_token against the UHC accounts management API. These guidelines codify the security conventions in the codebase so that contributors and AI agents produce consistent, secure changes.
The proxy sits between cluster operators and the Red Hat accounts management API. It receives a Bearer token and cluster ID from operator User-Agent headers, forwards them as an AccessToken credential to the upstream API, and returns an identity JSON used by downstream services. The proxy itself holds an offline access token (OAT) used to obtain short-lived access tokens from SSO.
Trust boundary: The proxy trusts nothing from the inbound HTTP request. Every request must pass User-Agent validation and Bearer token extraction before any upstream call is made.
Only requests from recognized operator prefixes are accepted. The allowlist is defined in server/server.go as operatorPrefixes.
Rules:
- Do not remove the
strings.HasPrefixcheck or thecluster/prefix requirement ingetClusterID. - When adding a new operator, add its prefix to the
operatorPrefixesarray and add a corresponding test case in thevalidOperatorAgentsslice inserver/server_test.go. - The User-Agent must match the format
<operator-prefix><version> cluster/<cluster_id>. Requests that do not match get a 400 response. - Do not fall back to a default cluster ID if parsing fails.
Rules:
- The
getTokenfunction inserver/server.gorequires the exact prefixBearer(with trailing space). Do not acceptBearer:or other variants. - The
getTokenerror message currently includes the raw authorization header value in the error string ("not a bearer token: '%s'"), and this error is logged viazap.Error. Avoid extending this pattern — do not log successfully extracted token values. If modifyinggetToken, consider redacting the header value in the error message. - The
makeKeyfunction rejects registrations where eitherClusterIDorAuthorizationTokenis empty, returning a 500. Do not weaken this check.
The in-memory cache in cache/cache.go uses clusterID:authorizationToken as the key.
Rules:
- The cache key must always include both the cluster ID and the full authorization token. This ensures that a stolen or rotated token cannot retrieve a cached identity from a previous token.
- Cache entries expire after 2 hours (hardcoded in
cache.Set). Do not increase this TTL without security review. - Always call
cache.Clear()in testBeforeEachblocks to prevent cross-test cache pollution.
The HTTPWrapper.AddHeaders method in requests/client/wrapper.go constructs the upstream authorization header:
req.Header.Add("Authorization", fmt.Sprintf("AccessToken %s:%s", cluster_id, authorization_token))Rules:
- Do not change this format without coordinating with the upstream accounts management API team.
- Do not add the raw
Bearertoken from the inbound request to outbound requests — only use theAccessTokenformat. - The HTTP client has a configurable timeout (
TIMEOUT_SECONDS). Do not set this to zero or remove the timeout.
The requests/client/access.go file manages the proxy's own SSO access token using the OAT (offline access token) environment variable.
Rules:
- The offline access token is fetched from the
OATenv var, sourced from a Kubernetes secret (uhc-auth-proxy-secret). Never hardcode this value. - The
CLIENT_IDis sourced from a secret and used in the SSO token request.CLIENT_SECRETis defined in the Kubernetes template but is not currently referenced in the Go code. Do not add defaults for these in config files. - Token refresh uses a mutex-protected package-level variable with expiry. Do not remove the
mutex.Lock()/defer mutex.Unlock()pattern — it prevents concurrent token refresh races. - The
ACCESS_TOKEN_URLdefault points tosso.redhat.com. Do not change this default to a non-HTTPS URL.
Rules:
- Error responses to clients should not leak internal URLs, stack traces, or upstream response bodies. The current pattern wraps errors with user-facing messages like
"Could not authenticate". Note thatgetTokencurrently includes the raw authorization header in its error message — exercise caution when modifying error messages to avoid expanding information disclosure. - When the upstream API returns an error body, it is parsed into
AccountErrorand logged server-side withzap.Objectfor structured diagnostics. The client receives only the reason, code, and ID fields viaAccountError.Error()— do not add raw upstream response bodies to client-facing output. - Use
errors.Asfor error type switching (seegetErrorStatusCode). Do not use type assertions directly. - Default to HTTP 401 when the upstream error type is unknown. Do not default to 200 or 500 for authentication failures.
All secrets are injected via environment variables sourced from Kubernetes secrets (see openshift/uhc-auth-proxy-template.yaml).
Rules:
- Sensitive env vars:
OAT,CLIENT_ID,CLIENT_SECRET,CW_AWS_ACCESS_KEY_ID,CW_AWS_SECRET_ACCESS_KEY. Never log these values. - Do not add
viper.SetDefaultfor any secret value. Defaults are only acceptable for non-sensitive configuration likeSERVER_PORT,TIMEOUT_SECONDS,LOG_LEVEL, CloudWatch log group/region/stream, and upstream API URLs. - The
.gitignoreand.dockerignoreshould not be modified to include secret files.
Rules:
- The server uses
chimiddleware:request_id.ConfiguredRequestID,RealIP,Logger,Recoverer, andStripSlashes. Do not removeRecoverer— it prevents panics from crashing the process and leaking stack traces. - The
/metricsendpoint exposes Prometheus metrics. It does not require authentication (it is accessed by internal scrapers), but it should not expose secret values in metric labels. - The
/statusendpoint is used for liveness/readiness probes. It must remain unauthenticated and lightweight. - The server binds to a configurable port (default 8080). It does not configure TLS directly — TLS termination is handled by the OpenShift router/ingress.
Rules:
- All logging uses structured JSON via
zap. Do not usefmt.Printlnfor operational logging (it is acceptable in CLIcmd/code). - Do not add
zap.String("token", ...)or log any field containing a Bearer token, OAT, or AWS secret key. Note thatgetTokencurrently includes the raw authorization header in its error string when the format is invalid — avoid extending this pattern to valid tokens. - The
cluster_idis safe to log and is already included in error logs. Theauthorization_tokenis not — maintain this distinction. - CloudWatch integration uses static AWS credentials. If modifying
logger/cloudwatch.go, ensure credentials are only read fromlogConfig(which sources from env vars), not from config files on disk.
Rules:
- Use
FakeWrapper,ErrorWrapper, andErrorWithBodyWrapper(defined inrequests/cluster/types.go) for test mocking. Do not make real HTTP calls to upstream APIs in unit tests. - Test both valid and invalid inputs for all parsing functions (
getClusterID,getToken,makeKey). The existing test suite covers empty auth, malformed User-Agent, and missing cluster prefixes. - When adding a new operator to the allowlist, add a test in
server/server_test.gothat exercises the full handler path (via thecallhelper function), not just thegetClusterIDparser. - Use
httptest.NewRecorderfor handler tests. Do not start a real HTTP server in unit tests.
Rules:
- The build uses Hummingbird FIPS-compliant images:
hi/go:1.26.4-fips-builderfor building andhi/core-runtime:2.42-openssl-fipsfor runtime. Do not switch to non-FIPS images without security approval. - FIPS 140 mode is enabled via
GODEBUG=fips140=on. This environment variable must remain set in the final image. - The binary is built with
CGO_ENABLED=0for a static binary. Do not enable CGO unless absolutely necessary. - The build stage runs as
rootfor dependency fetching and compilation. The final stage does not set a USER directive — consider addingUSER 1001if the deployment does not already enforce a non-root security context.
Rules:
- Dependencies are managed via
go.mod. Review any new dependency for security implications before adding it. - Automated dependency update PRs (from Mintmaker/Konflux) should be reviewed for breaking changes in security-sensitive packages (
net/http, TLS libraries, authentication libraries). - The
go.sumfile must always be committed alongsidego.modchanges to ensure integrity verification.