diff --git a/CHANGELOG.md b/CHANGELOG.md index 11985b62..03e4b0ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Use the `ServiceAccount` authorization scheme for HyperFleet API requests ([HYPERFLEET-1480](https://issues.redhat.com/browse/HYPERFLEET-1480)) - Migrate CEL dependency from `github.com/google/cel-go` to its renamed module path `cel.dev/cel-go` (v0.32.0), unblocking Renovate dependency-update PRs that were failing `go get -t ./...` ([HYPERFLEET-1578](https://issues.redhat.com/browse/HYPERFLEET-1578)) ## [0.2.0] - 2026-03-30 diff --git a/charts/README.md b/charts/README.md index e3f3b061..7285488e 100644 --- a/charts/README.md +++ b/charts/README.md @@ -32,8 +32,8 @@ helm install hyperfleet-adapter oci://REGISTRY/hyperfleet-adapter \ | adapterConfig.hyperfleetApi | object | `{"auth":{"audience":"hyperfleet-api","enabled":false,"expirationSeconds":3600,"tokenCacheTtl":"30s","tokenPath":"/var/run/secrets/hyperfleet/token"},"baseUrl":"http://hyperfleet-api:8000","version":"v1"}` | HyperFleet API connection settings injected as environment variables | | adapterConfig.hyperfleetApi.baseUrl | string | `"http://hyperfleet-api:8000"` | API base URL (`HYPERFLEET_API_BASE_URL`) | | adapterConfig.hyperfleetApi.version | string | `"v1"` | API version (`HYPERFLEET_API_VERSION`) | -| adapterConfig.hyperfleetApi.auth | object | `{"audience":"hyperfleet-api","enabled":false,"expirationSeconds":3600,"tokenCacheTtl":"30s","tokenPath":"/var/run/secrets/hyperfleet/token"}` | JWT bearer token authentication via Kubernetes projected ServiceAccount token | -| adapterConfig.hyperfleetApi.auth.enabled | bool | `false` | Enable bearer token auth (`HYPERFLEET_API_AUTH_TOKEN_PATH`) | +| adapterConfig.hyperfleetApi.auth | object | `{"audience":"hyperfleet-api","enabled":false,"expirationSeconds":3600,"tokenCacheTtl":"30s","tokenPath":"/var/run/secrets/hyperfleet/token"}` | ServiceAccount authentication via Kubernetes projected ServiceAccount token | +| adapterConfig.hyperfleetApi.auth.enabled | bool | `false` | Enable ServiceAccount token auth (`HYPERFLEET_API_AUTH_TOKEN_PATH`) | | adapterConfig.hyperfleetApi.auth.audience | string | `"hyperfleet-api"` | ServiceAccount token audience (used for the projected volume) | | adapterConfig.hyperfleetApi.auth.tokenPath | string | `"/var/run/secrets/hyperfleet/token"` | Absolute path where the token file is mounted | | adapterConfig.hyperfleetApi.auth.expirationSeconds | int | `3600` | Token lifetime in seconds for the projected ServiceAccount token | diff --git a/charts/values.yaml b/charts/values.yaml index 87660efb..a1cb8325 100644 --- a/charts/values.yaml +++ b/charts/values.yaml @@ -27,9 +27,9 @@ adapterConfig: baseUrl: http://hyperfleet-api:8000 # -- API version (`HYPERFLEET_API_VERSION`) version: v1 - # -- JWT bearer token authentication via Kubernetes projected ServiceAccount token + # -- ServiceAccount authentication via Kubernetes projected ServiceAccount token auth: - # -- Enable bearer token auth (`HYPERFLEET_API_AUTH_TOKEN_PATH`) + # -- Enable ServiceAccount token auth (`HYPERFLEET_API_AUTH_TOKEN_PATH`) enabled: false # -- ServiceAccount token audience (used for the projected volume) audience: hyperfleet-api diff --git a/cmd/adapter/main.go b/cmd/adapter/main.go index c35319bf..6b440289 100644 --- a/cmd/adapter/main.go +++ b/cmd/adapter/main.go @@ -327,7 +327,7 @@ func createAPIClient(apiConfig configloader.HyperfleetAPIConfig) (hyperfleetapi. opts = append(opts, hyperfleetapi.WithDefaultHeader(key, value)) } - // Configure bearer token auth if set + // Configure ServiceAccount auth if set if apiConfig.Auth != nil { opts = append(opts, hyperfleetapi.WithAuth(apiConfig.Auth)) } diff --git a/configs/adapter-config-template.yaml b/configs/adapter-config-template.yaml index b84f5840..222ab678 100644 --- a/configs/adapter-config-template.yaml +++ b/configs/adapter-config-template.yaml @@ -114,8 +114,8 @@ clients: timeout: 2s retry_attempts: 3 retry_backoff: exponential - # Optional JWT bearer token authentication via a file (e.g. Kubernetes projected ServiceAccount token). - # When configured, the token is read from token_path and attached as Authorization: Bearer . + # Optional ServiceAccount authentication via a token file (e.g. Kubernetes projected ServiceAccount token). + # When configured, the token is read from token_path and attached as Authorization: ServiceAccount . # token_path must be an absolute path. # Environment variables: HYPERFLEET_API_AUTH_TOKEN_PATH, HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL # auth: diff --git a/docs/adapter-authoring-guide.md b/docs/adapter-authoring-guide.md index 7da727bf..3ddaa5b0 100644 --- a/docs/adapter-authoring-guide.md +++ b/docs/adapter-authoring-guide.md @@ -272,7 +272,7 @@ File-sourced params can be referenced in `api_call` headers via Go Templates: url: "/clusters/{{ .clusterId }}" headers: - name: "Authorization" - value: "Bearer {{ .k8sToken }}" + value: "ServiceAccount {{ .k8sToken }}" ``` > **Security:** File-sourced tokens rendered into headers carry credentials. Ensure request/response logging (including reverse proxies and service meshes) does not capture `Authorization` or other sensitive headers. diff --git a/docs/configuration.md b/docs/configuration.md index 90490858..b4bd88ea 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -115,7 +115,7 @@ clients: - `base_delay` (duration string): Initial retry delay. Default: `1s`. - `max_delay` (duration string): Maximum retry delay. Default: `30s`. - `default_headers` (map[string]string): Headers added to all API requests. -- `auth.token_path` (string): Absolute path to a file containing a JWT bearer token. When set, the token is read from this file and attached as `Authorization: Bearer ` on every request. Typically a Kubernetes projected ServiceAccount token. Must be an absolute path. +- `auth.token_path` (string): Absolute path to a file containing a ServiceAccount token. When set, the token is read from this file and attached as `Authorization: ServiceAccount ` on every request. Typically a Kubernetes projected ServiceAccount token. Must be an absolute path. - `auth.token_cache_ttl` (duration string): How long the token is cached in memory before re-reading the file. Zero (default) means re-read on every request. ### Broker (`clients.broker`) diff --git a/docs/deployment.md b/docs/deployment.md index 6ca3bc55..a7bc0dff 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -84,7 +84,7 @@ These fields have first-class Helm values that the chart injects as environment | `adapterConfig.log.level` | Log level (`debug`, `info`, `warn`, `error`) | `LOG_LEVEL` | `info` | | `adapterConfig.hyperfleetApi.baseUrl` | HyperFleet API base URL | `HYPERFLEET_API_BASE_URL` | `http://hyperfleet-api:8000` | | `adapterConfig.hyperfleetApi.version` | API version | `HYPERFLEET_API_VERSION` | `v1` | -| `adapterConfig.hyperfleetApi.auth.enabled` | Enable JWT bearer token auth | — (controls volume + env vars) | `false` | +| `adapterConfig.hyperfleetApi.auth.enabled` | Enable ServiceAccount token auth | — (controls volume + env vars) | `false` | | `adapterConfig.hyperfleetApi.auth.tokenPath` | Absolute path to the token file | `HYPERFLEET_API_AUTH_TOKEN_PATH` | `/var/run/secrets/hyperfleet/token` | | `adapterConfig.hyperfleetApi.auth.tokenCacheTtl` | In-memory token cache TTL | `HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL` | `30s` | | `adapterConfig.hyperfleetApi.auth.audience` | ServiceAccount token audience | — (used in projected volume) | `hyperfleet-api` | @@ -147,12 +147,12 @@ When using individual properties, `broker.type` must be set to `googlepubsub` or ## HyperFleet API Authentication -The adapter can authenticate to the HyperFleet API using a Kubernetes projected ServiceAccount token (JWT bearer token). Authentication is **disabled by default** — existing deployments are unaffected. +The adapter can authenticate to the HyperFleet API using a Kubernetes projected ServiceAccount token. Authentication is **disabled by default** — existing deployments are unaffected. When enabled, the Helm chart: 1. Mounts a projected `serviceAccountToken` volume at the configured `tokenPath` directory. 2. Sets `HYPERFLEET_API_AUTH_TOKEN_PATH` and `HYPERFLEET_API_AUTH_TOKEN_CACHE_TTL` env vars. -3. The adapter reads the token file and attaches `Authorization: Bearer ` to every HyperFleet API request. +3. The adapter reads the token file and attaches `Authorization: ServiceAccount ` to every HyperFleet API request. ```yaml adapterConfig: diff --git a/internal/executor/post_action_executor_test.go b/internal/executor/post_action_executor_test.go index 819b86d4..6414e838 100644 --- a/internal/executor/post_action_executor_test.go +++ b/internal/executor/post_action_executor_test.go @@ -557,7 +557,7 @@ func TestExecuteAPICall(t *testing.T) { Method: "GET", URL: "http://api.example.com/clusters", Headers: []configloader.Header{ - {Name: "Authorization", Value: "Bearer {{ .token }}"}, + {Name: "Authorization", Value: "ServiceAccount {{ .token }}"}, {Name: "X-Request-ID", Value: "{{ .requestId }}"}, }, }, diff --git a/internal/hyperfleetapi/client.go b/internal/hyperfleetapi/client.go index 2a330b45..bf2400af 100644 --- a/internal/hyperfleetapi/client.go +++ b/internal/hyperfleetapi/client.go @@ -111,7 +111,7 @@ func WithBaseURL(baseURL string) ClientOption { } } -// WithAuth configures JWT bearer token authentication from a file. +// WithAuth configures ServiceAccount authentication from a token file. func WithAuth(auth *AuthConfig) ClientOption { return func(c *httpClient) { c.config.Auth = auth @@ -157,7 +157,7 @@ func NewClient(opts ...ClientOption) (Client, error) { } } - // Initialize token source for bearer token auth if configured + // Initialize the token source for ServiceAccount auth if configured if c.config.Auth != nil && c.config.Auth.TokenPath != "" { c.tokenSource = newFileTokenSource(c.config.Auth.TokenPath, c.config.Auth.TokenCacheTTL) } @@ -334,13 +334,13 @@ func (c *httpClient) doRequest(ctx context.Context, req *Request) (*Response, er httpReq.Header.Set(k, v) } - // Inject bearer token auth header + // Inject the ServiceAccount auth header if c.tokenSource != nil { tok, authErr := c.tokenSource.get() if authErr != nil { return nil, fmt.Errorf("getting auth token: %w", authErr) } - httpReq.Header.Set("Authorization", "Bearer "+tok) + httpReq.Header.Set("Authorization", "ServiceAccount "+tok) } // Set default Content-Type for requests with body diff --git a/internal/hyperfleetapi/client_test.go b/internal/hyperfleetapi/client_test.go index 9f072101..6f18a7c5 100644 --- a/internal/hyperfleetapi/client_test.go +++ b/internal/hyperfleetapi/client_test.go @@ -208,7 +208,7 @@ func TestClientWithHeaders(t *testing.T) { defer server.Close() client, err := NewClient(WithBaseURL(server.URL), - WithDefaultHeader("Authorization", "Bearer default-token")) + WithDefaultHeader("Authorization", "ServiceAccount default-token")) require.NoError(t, err, "failed to create client") ctx := context.Background() @@ -218,8 +218,8 @@ func TestClientWithHeaders(t *testing.T) { ) require.NoError(t, err, "unexpected error") - if receivedAuth != "Bearer default-token" { - t.Errorf("expected Authorization header 'Bearer default-token', got %q", receivedAuth) + if receivedAuth != "ServiceAccount default-token" { + t.Errorf("expected Authorization header 'ServiceAccount default-token', got %q", receivedAuth) } if receivedCustom != "custom-value" { @@ -667,10 +667,10 @@ func TestAPIErrorInRetryExhausted(t *testing.T) { } } -func TestClientBearerTokenAuth(t *testing.T) { +func TestClientServiceAccountTokenAuth(t *testing.T) { dir := t.TempDir() tokenFile := filepath.Join(dir, "token") - if err := os.WriteFile(tokenFile, []byte("test-jwt-token"), 0600); err != nil { + if err := os.WriteFile(tokenFile, []byte("test-token"), 0600); err != nil { t.Fatal(err) } @@ -690,8 +690,8 @@ func TestClientBearerTokenAuth(t *testing.T) { _, err = client.Get(context.Background(), "/test") require.NoError(t, err) - if receivedAuth != "Bearer test-jwt-token" { - t.Errorf("Authorization = %q, want %q", receivedAuth, "Bearer test-jwt-token") + if receivedAuth != "ServiceAccount test-token" { + t.Errorf("Authorization = %q, want %q", receivedAuth, "ServiceAccount test-token") } } diff --git a/internal/hyperfleetapi/token.go b/internal/hyperfleetapi/token.go index d2282421..557157f7 100644 --- a/internal/hyperfleetapi/token.go +++ b/internal/hyperfleetapi/token.go @@ -8,7 +8,7 @@ import ( "time" ) -// fileTokenSource reads a bearer token from disk on every call, or caches it +// fileTokenSource reads a ServiceAccount token from disk on every call, or caches it // for cacheTTL when cacheTTL > 0. A zero cacheTTL disables caching and causes // the file to be re-read on every request. It is safe for concurrent use. type fileTokenSource struct { diff --git a/internal/hyperfleetapi/types.go b/internal/hyperfleetapi/types.go index 1acd0f22..35838f78 100644 --- a/internal/hyperfleetapi/types.go +++ b/internal/hyperfleetapi/types.go @@ -34,11 +34,11 @@ const ( // Client Configuration // ----------------------------------------------------------------------------- -// AuthConfig holds optional JWT bearer token authentication configuration. -// When set, a bearer token is read from TokenPath and injected as an -// Authorization header on every outbound request. +// AuthConfig holds optional ServiceAccount authentication configuration. +// When set, a token is read from TokenPath and injected using the +// ServiceAccount authorization scheme on every outbound request. type AuthConfig struct { - // TokenPath is the absolute path to a file containing the bearer token. + // TokenPath is the absolute path to a file containing the ServiceAccount token. TokenPath string `yaml:"token_path,omitempty" mapstructure:"token_path"` // TokenCacheTTL controls how long the token is cached in memory. // Zero means the file is re-read on every request. @@ -49,7 +49,7 @@ type AuthConfig struct { type ClientConfig struct { // DefaultHeaders are headers added to all requests DefaultHeaders map[string]string `yaml:"default_headers,omitempty" mapstructure:"default_headers"` - // Auth configures optional JWT bearer token authentication. + // Auth configures optional ServiceAccount authentication. // When nil, requests are sent without an Authorization header. Auth *AuthConfig `yaml:"auth,omitempty" mapstructure:"auth"` // BaseURL is the base URL for all API requests (must be set by caller)