Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 4 additions & 10 deletions charts/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,9 +44,9 @@ helm install hyperfleet-api oci://REGISTRY/hyperfleet-api \
| ports.api | int | `8000` | API server port |
| ports.health | int | `8080` | Health check endpoint port |
| ports.metrics | int | `9090` | Prometheus metrics endpoint port |
| config | object | `{"adapters":{"required":{"cluster":[],"nodepool":[]}},"database":{"debug":false,"dialect":"postgres","host":"","name":"hyperfleet","pool":{"conn_max_idle_time":"1m","conn_max_lifetime":"5m","conn_retry_attempts":10,"conn_retry_interval":"3s","max_connections":50,"max_idle_connections":10,"request_timeout":"30s"},"port":5432,"ssl":{"mode":"disable","root_cert_file":""}},"entities":[{"kind":"Channel","plural":"channels","search_disallowed_fields":["spec"],"spec_schema_name":"ChannelSpec"},{"kind":"Version","on_parent_delete":"restrict","parent_kind":"Channel","plural":"versions","search_disallowed_fields":["spec"],"spec_schema_name":"VersionSpec"},{"kind":"WifConfig","plural":"wifconfigs","search_disallowed_fields":["spec"],"spec_schema_name":"WifConfigSpec"}],"existingConfigMap":"","health":{"db_ping_timeout":"2s","host":"0.0.0.0","port":8080,"shutdown_timeout":"20s","tls":{"enabled":false}},"logging":{"format":"json","level":"info","masking":{"enabled":true,"fields":["password","secret","token","api_key","access_token","refresh_token","client_secret"],"headers":["Authorization","X-API-Key","Cookie","X-Auth-Token","X-Forwarded-Authorization","X-HyperFleet-Identity"]},"otel":{"enabled":false},"output":"stdout"},"metrics":{"host":"0.0.0.0","label_metrics_inclusion_duration":"168h","port":9090,"reconciliation_stuck_threshold":"10m","tls":{"enabled":false}},"server":{"host":"0.0.0.0","hostname":"","identity_header":"","jwk":{"cert_file":"","cert_url":""},"jwt":{"audience":"","enabled":false,"identity_claim":"email","issuer_url":""},"port":8000,"timeouts":{"read":"5s","write":"30s"},"tls":{"cert_file":"","enabled":false,"key_file":""}}}` | Application configuration. All settings in this section generate the ConfigMap consumed by the API server. Set `config.existingConfigMap` to use a pre-existing ConfigMap instead. |
| config | object | `{"adapters":{"required":{"cluster":[],"nodepool":[]}},"database":{"debug":false,"dialect":"postgres","host":"","name":"hyperfleet","pool":{"conn_max_idle_time":"1m","conn_max_lifetime":"5m","conn_retry_attempts":10,"conn_retry_interval":"3s","max_connections":50,"max_idle_connections":10,"request_timeout":"30s"},"port":5432,"ssl":{"mode":"disable","root_cert_file":""}},"entities":[{"kind":"Channel","plural":"channels","search_disallowed_fields":["spec"],"spec_schema_name":"ChannelSpec"},{"kind":"Version","on_parent_delete":"restrict","parent_kind":"Channel","plural":"versions","search_disallowed_fields":["spec"],"spec_schema_name":"VersionSpec"},{"kind":"WifConfig","plural":"wifconfigs","search_disallowed_fields":["spec"],"spec_schema_name":"WifConfigSpec"}],"existingConfigMap":"","health":{"db_ping_timeout":"2s","host":"0.0.0.0","port":8080,"shutdown_timeout":"20s","tls":{"enabled":false}},"logging":{"format":"json","level":"info","masking":{"enabled":true,"fields":["password","secret","token","api_key","access_token","refresh_token","client_secret"],"headers":["Authorization","X-API-Key","Cookie","X-Auth-Token","X-Forwarded-Authorization","X-HyperFleet-Identity"]},"otel":{"enabled":false},"output":"stdout"},"metrics":{"host":"0.0.0.0","label_metrics_inclusion_duration":"168h","port":9090,"reconciliation_stuck_threshold":"10m","tls":{"enabled":false}},"server":{"host":"0.0.0.0","hostname":"","jwt":{"configs":[],"enabled":false},"port":8000,"timeouts":{"read":"5s","write":"30s"},"tls":{"cert_file":"","enabled":false,"key_file":""}}}` | Application configuration. All settings in this section generate the ConfigMap consumed by the API server. Set `config.existingConfigMap` to use a pre-existing ConfigMap instead. |
| config.existingConfigMap | string | `""` | Use an existing ConfigMap instead of generating one. When set, all other `config.*` values are ignored. |
| config.server | object | `{"host":"0.0.0.0","hostname":"","identity_header":"","jwk":{"cert_file":"","cert_url":""},"jwt":{"audience":"","enabled":false,"identity_claim":"email","issuer_url":""},"port":8000,"timeouts":{"read":"5s","write":"30s"},"tls":{"cert_file":"","enabled":false,"key_file":""}}` | HTTP server settings |
| config.server | object | `{"host":"0.0.0.0","hostname":"","jwt":{"configs":[],"enabled":false},"port":8000,"timeouts":{"read":"5s","write":"30s"},"tls":{"cert_file":"","enabled":false,"key_file":""}}` | HTTP server settings |
| config.server.hostname | string | `""` | Public hostname advertised by the API (leave empty for auto-detect) |
| config.server.host | string | `"0.0.0.0"` | Listen address |
| config.server.port | int | `8000` | Listen port (must match `ports.api`) |
Expand All @@ -57,15 +57,9 @@ helm install hyperfleet-api oci://REGISTRY/hyperfleet-api \
| config.server.tls.enabled | bool | `false` | Enable TLS on the API listener |
| config.server.tls.cert_file | string | `""` | Path to TLS certificate file |
| config.server.tls.key_file | string | `""` | Path to TLS key file |
| config.server.jwt | object | `{"audience":"","enabled":false,"identity_claim":"email","issuer_url":""}` | JWT authentication settings |
| config.server.jwt | object | `{"configs":[],"enabled":false}` | JWT authentication settings |
| config.server.jwt.enabled | bool | `false` | Enable JWT authentication |
| config.server.jwt.issuer_url | string | `""` | OIDC issuer URL for token validation |
| config.server.jwt.audience | string | `""` | Expected JWT audience claim |
| config.server.jwt.identity_claim | string | `"email"` | JWT claim used as the caller identity |
| config.server.identity_header | string | `""` | HTTP header used to pass caller identity (bypasses JWT when set) |
| config.server.jwk | object | `{"cert_file":"","cert_url":""}` | JWK settings for token verification |
| config.server.jwk.cert_file | string | `""` | Path to a local JWK certificate file |
| config.server.jwk.cert_url | string | `""` | URL to fetch JWK certificates from |
| config.server.jwt.configs | list | `[]` | List of JWT issuer configurations (multi-issuer support) |
| config.database | object | `{"debug":false,"dialect":"postgres","host":"","name":"hyperfleet","pool":{"conn_max_idle_time":"1m","conn_max_lifetime":"5m","conn_retry_attempts":10,"conn_retry_interval":"3s","max_connections":50,"max_idle_connections":10,"request_timeout":"30s"},"port":5432,"ssl":{"mode":"disable","root_cert_file":""}}` | Database connection settings. Credentials must be provided via a Secret — see `database.external.secretName` or use the built-in PostgreSQL (`database.postgresql.enabled`). |
| config.database.dialect | string | `"postgres"` | SQL dialect |
| config.database.host | string | `""` | Database host (auto-set when using the built-in PostgreSQL) |
Expand Down
22 changes: 13 additions & 9 deletions charts/templates/NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,28 @@ Validation schema validation is ENABLED.
{{- end }}

Caller identity (audit attribution):
config.server.jwt.identity_claim: {{ .Values.config.server.jwt.identity_claim | default "" | quote }}
config.server.identity_header: {{ .Values.config.server.identity_header | default "" | quote }}
JWT issuers configured: {{ len .Values.config.server.jwt.configs }}

When identity_header is set and a request includes a non-empty header with that name,
the header value overrides the JWT claim for audit fields (created_by, updated_by).
When identity_claim is set, the named JWT claim is used as the caller identity.
Each JWT issuer config can specify its own identity_claim (defaults to "email") and
identity_header. When identity_header is set per-issuer, a matching non-empty header
value overrides the JWT claim for audit fields (created_by, updated_by, deleted_by).
Only trusted gateways should set the identity header in production.

Override in values.yaml:
config:
server:
jwt:
identity_claim: preferred_username
identity_header: X-HyperFleet-Identity
enabled: true
configs:
- issuer_url: "https://idp.example.com/auth/realms/my-realm"
jwk_cert_url: "https://idp.example.com/certs"

Or at install/upgrade:
--set config.server.jwt.identity_claim=preferred_username
--set config.server.identity_header=X-HyperFleet-Identity
--set config.server.jwt.enabled=true
--set-json 'config.server.jwt.configs=[{"issuer_url":"https://idp.example.com","jwk_cert_url":"https://idp.example.com/certs"}]'

Full issuer field reference (header, audience, identity_claim, identity_claim_pattern, identity_header):
https://github.com/openshift-hyperfleet/hyperfleet-api/blob/main/docs/authentication.md#issuer-configuration-reference

Documentation:
https://github.com/openshift-hyperfleet/hyperfleet-api/blob/main/docs/deployment.md
36 changes: 27 additions & 9 deletions charts/templates/configmap.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -28,15 +28,33 @@ data:

jwt:
enabled: {{ .Values.config.server.jwt.enabled }}
issuer_url: {{ .Values.config.server.jwt.issuer_url | quote }}
audience: {{ .Values.config.server.jwt.audience | quote }}
identity_claim: {{ .Values.config.server.jwt.identity_claim | quote }}

identity_header: {{ .Values.config.server.identity_header | quote }}

jwk:
cert_file: {{ .Values.config.server.jwk.cert_file | quote }}
cert_url: {{ .Values.config.server.jwk.cert_url | quote }}
{{- if .Values.config.server.jwt.configs }}
configs:
{{- range .Values.config.server.jwt.configs }}
- issuer_url: {{ .issuer_url | quote }}
{{- if .jwk_cert_url }}
jwk_cert_url: {{ .jwk_cert_url | quote }}
{{- end }}
{{- if .jwk_cert_file }}
jwk_cert_file: {{ .jwk_cert_file | quote }}
{{- end }}
{{- if .header }}
header: {{ .header | quote }}
{{- end }}
{{- if .audience }}
audience: {{ .audience | quote }}
{{- end }}
{{- if .identity_claim }}
identity_claim: {{ .identity_claim | quote }}
{{- end }}
{{- if .identity_claim_pattern }}
identity_claim_pattern: {{ .identity_claim_pattern | quote }}
{{- end }}
{{- if .identity_header }}
identity_header: {{ .identity_header | quote }}
{{- end }}
{{- end }}
{{- end }}

database:
dialect: {{ .Values.config.database.dialect }}
Expand Down
70 changes: 41 additions & 29 deletions charts/values.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -145,35 +145,47 @@
"type": "boolean",
"description": "Enable JWT token validation"
},
"issuer_url": {
"type": "string",
"description": "Expected JWT issuer URL"
},
"audience": {
"type": "string",
"description": "Expected JWT audience claim"
},
"identity_claim": {
"type": "string",
"description": "JWT claim to extract the caller identity from (e.g. email, sub)"
}
}
},
"identity_header": {
"type": "string",
"description": "HTTP header carrying a pre-authenticated identity (bypasses JWT when set)"
},
"jwk": {
"type": "object",
"description": "JWK key material for token verification",
"properties": {
"cert_file": {
"type": "string",
"description": "Path to a local JWK certificate file"
},
"cert_url": {
"type": "string",
"description": "URL to fetch JWK certificates from"
"configs": {
"type": "array",
"description": "List of JWT issuer configurations (multi-issuer support)",
"items": {
"type": "object",
"properties": {
"issuer_url": {
"type": "string",
"description": "OIDC issuer URL for token validation (required)"
},
"jwk_cert_url": {
"type": "string",
"description": "URL to fetch JWK certificates from"
},
"jwk_cert_file": {
"type": "string",
"description": "Path to a local JWK certificate file"
},
"header": {
"type": "string",
"description": "HTTP header to read the JWT from (default: Authorization)"
},
"audience": {
"type": "string",
"description": "Expected JWT audience claim"
},
"identity_claim": {
"type": "string",
"description": "JWT claim to extract the caller identity from (default: email)"
},
"identity_claim_pattern": {
"type": "string",
"description": "Regex pattern to validate the identity claim value"
},
"identity_header": {
"type": "string",
"description": "HTTP header carrying a pre-authenticated identity (bypasses JWT claim when set)"
}
},
"required": ["issuer_url"]
}
}
}
}
Expand Down
29 changes: 13 additions & 16 deletions charts/values.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -69,22 +69,19 @@ config:
jwt:
# -- Enable JWT authentication
enabled: false
# -- OIDC issuer URL for token validation
issuer_url: ""
# -- Expected JWT audience claim
audience: ""
# -- JWT claim used as the caller identity
identity_claim: email

# -- HTTP header used to pass caller identity (bypasses JWT when set)
identity_header: ""

# -- JWK settings for token verification
jwk:
# -- Path to a local JWK certificate file
cert_file: ""
# -- URL to fetch JWK certificates from
cert_url: ""
# -- List of JWT issuer configurations (multi-issuer support)
configs: []
# Example:
# configs:
# - issuer_url: "https://idp.example.com/auth/realms/my-realm"
# jwk_cert_url: "https://idp.example.com/certs"
# audience: "my-api"
# identity_claim: "email"
# identity_claim_pattern: "^[^@]+@example\\.com$"
# identity_header: "X-HyperFleet-Identity"
# - issuer_url: "https://other-idp.example.com"
# jwk_cert_file: "/etc/hyperfleet/jwks.json"
# header: "X-Custom-Auth"

# -- Database connection settings. Credentials must be provided via a
# Secret — see `database.external.secretName` or use the built-in
Expand Down
12 changes: 12 additions & 0 deletions cmd/hyperfleet-api/environments/e_integration_testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,18 @@ func (e *integrationTestingEnvImpl) OverrideConfig(c *config.ApplicationConfig)
c.Database.SSL.Mode = SSLModeDisable
}

// Bootstrap a default JWT issuer for integration tests.
// JWKCertURL is overridden by the JWK mock server in test.Helper.startAPIServer().
if c.Server.JWT.Enabled && len(c.Server.JWT.Configs) == 0 {
c.Server.JWT.Configs = []config.JWTIssuerConfig{
{
IssuerURL: "https://test-issuer.example.com",
Header: "Authorization",
IdentityClaim: "email",
},
}
}

return nil
}

Expand Down
5 changes: 1 addition & 4 deletions cmd/hyperfleet-api/server/api_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,7 @@ func NewAPIServer(tracingEnabled bool) Server {

if env().Config.Server.JWT.Enabled {
jwtHandler, err := auth.NewJWTHandler(context.Background(), auth.JWTHandlerConfig{
KeysFile: env().Config.Server.JWK.CertFile,
KeysURL: env().Config.Server.JWK.CertURL,
IssuerURL: env().Config.Server.JWT.IssuerURL,
Audience: env().Config.Server.JWT.Audience,
Issuers: env().Config.Server.JWT.Configs,
PublicPaths: []string{
"^/api/hyperfleet/?$",
"^/api/hyperfleet/v1/?$",
Expand Down
11 changes: 2 additions & 9 deletions cmd/hyperfleet-api/server/routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,15 +87,8 @@ func (s *apiServer) routes(tracingEnabled bool) *mux.Router {
err = registerAPIMiddleware(apiV1Router)
check(err, "Failed to initialize API middleware")

identityCfg := auth.CallerIdentityConfig{
HeaderName: env().Config.Server.IdentityHeader,
}
if env().Config.Server.JWT.Enabled && env().Config.Server.JWT.IdentityClaim != "" {
identityCfg.JWTIdentityClaim = env().Config.Server.JWT.IdentityClaim
}
if identityCfg.JWTIdentityClaim != "" || identityCfg.HeaderName != "" {
callerIdentityMW, mwErr := auth.NewCallerIdentityMiddleware(identityCfg)
check(mwErr, "Unable to create caller identity middleware")
if env().Config.Server.JWT.Enabled {
callerIdentityMW := auth.NewCallerIdentityMiddleware()
apiV1Router.Use(callerIdentityMW.ResolveCallerIdentity)
}

Expand Down
18 changes: 9 additions & 9 deletions configs/config.yaml.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,15 +19,15 @@ server:

jwt:
enabled: true # Enable JWT authentication
issuer_url: "" # JWT issuer URL (required when jwt.enabled=true)
audience: "" # JWT audience claim (optional)
identity_claim: email # JWT claim used as request identity for audit (e.g. email, preferred_username, sub)

identity_header: "" # HTTP header name for caller identity; leave empty to disable (e.g. X-HyperFleet-Identity)

jwk:
cert_file: "" # JWK certificate file path
cert_url: "" # JWK certificate URL (required when jwt.enabled=true and cert_file is not set)
configs: # List of JWT issuer configurations (required when jwt.enabled=true)
- issuer_url: "" # OIDC issuer URL for token validation (required)
jwk_cert_url: "" # URL to fetch JWK certificates from (required unless jwk_cert_file is set)
jwk_cert_file: "" # Path to a local JWK certificate file (alternative to jwk_cert_url)
header: Authorization # HTTP header to read the JWT from (default: Authorization)
audience: "" # Expected JWT audience claim (optional)
identity_claim: email # JWT claim used as request identity for audit (default: email)
identity_claim_pattern: "" # Regex pattern to validate the identity claim value (optional)
identity_header: "" # Per-issuer HTTP header for caller identity; overrides JWT claim when set (e.g. X-HyperFleet-Identity)

# Database Configuration
database:
Expand Down
13 changes: 6 additions & 7 deletions docs/api-operator-guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -639,12 +639,13 @@ server:
enabled: false

# Production (with JWT authentication)
# See "Issuer configuration reference" in docs/authentication.md
server:
jwt:
enabled: true
issuer_url: https://your-idp.example.com/auth/realms/your-realm
jwk:
cert_url: https://your-idp.example.com/auth/realms/your-realm/protocol/openid-connect/certs
configs:
- issuer_url: https://your-idp.example.com/auth/realms/your-realm
jwk_cert_url: https://your-idp.example.com/auth/realms/your-realm/protocol/openid-connect/certs
```

**Direct binary execution:**
Expand All @@ -654,10 +655,8 @@ server:
./hyperfleet-api serve --server-jwt-enabled=false

# Production (with JWT authentication)
./hyperfleet-api serve \
--server-jwt-enabled=true \
--server-jwt-issuer-url=https://your-idp.example.com/auth/realms/your-realm \
--server-jwk-cert-url=https://your-idp.example.com/auth/realms/your-realm/protocol/openid-connect/certs
# Configure issuers in a config file
./hyperfleet-api serve --config config.yaml
```

See [Authentication Guide](authentication.md) for detailed JWT setup.
Expand Down
Loading