feat: auth & access - SSO support, TLS, and signing-key rotation (FB-896)#39
feat: auth & access - SSO support, TLS, and signing-key rotation (FB-896)#39kop wants to merge 24 commits into
Conversation
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Negative JWT durations pass admission
- FireboltInstance auth validation now rejects negative JWT durations, including maxTokenAge values that could bypass signing-key retain safety checks.
Or push these changes by commenting:
@cursor push 7dbede6aa7
Preview (7dbede6aa7)
diff --git a/api/v1alpha1/fireboltinstance_webhook.go b/api/v1alpha1/fireboltinstance_webhook.go
--- a/api/v1alpha1/fireboltinstance_webhook.go
+++ b/api/v1alpha1/fireboltinstance_webhook.go
@@ -245,7 +245,7 @@
// validateLocalAuth checks the admin-password reference and signing-key
// policy that Enabled=true requires, the chosen JWT signing algorithm's
// compatibility with the signing key's cert-manager algorithm, and the
-// format of the embedded server's optional JWT duration fields.
+// validity of the embedded server's optional JWT duration fields.
func validateLocalAuth(local *LocalAuthSpec, base *field.Path) field.ErrorList {
var errs field.ErrorList
@@ -269,13 +269,13 @@
errs = append(errs, validateSigningKeyRotation(local, base.Child("signingKeys"))...)
}
- if err := validateDurationField(base.Child("tokenExpiry"), local.TokenExpiry); err != nil {
+ if err := validateNonNegativeDurationField(base.Child("tokenExpiry"), local.TokenExpiry); err != nil {
errs = append(errs, err)
}
- if err := validateDurationField(base.Child("maxTokenAge"), local.MaxTokenAge); err != nil {
+ if err := validateNonNegativeDurationField(base.Child("maxTokenAge"), local.MaxTokenAge); err != nil {
errs = append(errs, err)
}
- if err := validateDurationField(base.Child("clockSkewTolerance"), local.ClockSkewTolerance); err != nil {
+ if err := validateNonNegativeDurationField(base.Child("clockSkewTolerance"), local.ClockSkewTolerance); err != nil {
errs = append(errs, err)
}
@@ -293,10 +293,10 @@
if oidc.JWT != nil {
jwtPath := base.Child("jwt")
- if err := validateDurationField(jwtPath.Child("clockSkewTolerance"), oidc.JWT.ClockSkewTolerance); err != nil {
+ if err := validateNonNegativeDurationField(jwtPath.Child("clockSkewTolerance"), oidc.JWT.ClockSkewTolerance); err != nil {
errs = append(errs, err)
}
- if err := validateDurationField(jwtPath.Child("maxTokenAge"), oidc.JWT.MaxTokenAge); err != nil {
+ if err := validateNonNegativeDurationField(jwtPath.Child("maxTokenAge"), oidc.JWT.MaxTokenAge); err != nil {
errs = append(errs, err)
}
}
@@ -365,6 +365,22 @@
return nil
}
+// validateNonNegativeDurationField is validateDurationField plus a
+// non-negativity check for JWT durations whose defaults are zero or greater.
+func validateNonNegativeDurationField(path *field.Path, value string) *field.Error {
+ if value == "" {
+ return nil
+ }
+ d, err := parsePackdbDuration(value)
+ if err != nil {
+ return field.Invalid(path, value, `must be a valid duration string (e.g. "30s", "1h", "1d")`)
+ }
+ if d < 0 {
+ return field.Invalid(path, value, "must not be negative")
+ }
+ return nil
+}
+
// validatePositiveDurationField is validateDurationField plus a positivity
// check, for the one duration field (oidc.providers[].discovery.refreshInterval)
// packdb itself rejects when non-positive.
diff --git a/api/v1alpha1/fireboltinstance_webhook_test.go b/api/v1alpha1/fireboltinstance_webhook_test.go
--- a/api/v1alpha1/fireboltinstance_webhook_test.go
+++ b/api/v1alpha1/fireboltinstance_webhook_test.go
@@ -520,6 +520,18 @@
wantError: true,
},
{
+ name: "negative local jwt duration is rejected",
+ auth: &AuthSpec{
+ Enabled: true,
+ Local: &LocalAuthSpec{
+ Admin: validAdminSpec(),
+ SigningKeys: validSigningKeys(),
+ TokenExpiry: "-1h",
+ },
+ },
+ wantError: true,
+ },
+ {
name: "oidc.jwt duration fields validated the same way as local's",
auth: &AuthSpec{
Enabled: true,
@@ -534,6 +546,20 @@
wantError: true,
},
{
+ name: "negative oidc jwt duration is rejected",
+ auth: &AuthSpec{
+ Enabled: true,
+ Local: &LocalAuthSpec{Admin: validAdminSpec(), SigningKeys: validSigningKeys()},
+ OIDC: &OIDCAuthSpec{
+ JWT: &OIDCJWTSpec{MaxTokenAge: "-1d"},
+ Providers: []OIDCProviderSpec{
+ {Name: "okta", DiscoveryURL: "https://okta.example.com/.well-known/openid-configuration", UsernameMapping: "{{ email }}"},
+ },
+ },
+ },
+ wantError: true,
+ },
+ {
name: "oidc provider jwks.cacheTTL and discovery.refreshInterval accept packdb's days unit",
auth: &AuthSpec{
Enabled: true,
@@ -637,6 +663,22 @@
wantError: true,
},
{
+ name: "negative maxTokenAge cannot lower signing key retain floor",
+ auth: &AuthSpec{
+ Enabled: true,
+ Local: &LocalAuthSpec{
+ Admin: validAdminSpec(),
+ MaxTokenAge: "-1d",
+ SigningKeys: &SigningKeyPolicy{
+ CertManager: validSigningKeys().CertManager,
+ RotationInterval: &metav1.Duration{Duration: 30 * 24 * time.Hour},
+ RetainDuration: &metav1.Duration{Duration: time.Hour},
+ },
+ },
+ },
+ wantError: true,
+ },
+ {
name: "rotationInterval with retainDuration at least maxTokenAge is valid",
auth: &AuthSpec{
Enabled: true,You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit e8dcaf0. Configure here.
5833411 to
9adee73
Compare
9adee73 to
d3eeb31
Compare
d3eeb31 to
0e51778
Compare
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, and Cursor Security Agent left unresolved medium-severity findings (gateway TLS secret/key exposure and TLS downgrade window). Assigned gm42 and fstr for human review.
Sent by Cursor Approval Agent: Pull Request Router and Approver
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, and Cursor Security Agent left unresolved medium-severity findings (gateway TLS secret/key exposure and TLS downgrade window). Reviewers gm42 and fstr are already assigned.
Sent by Cursor Approval Agent: Pull Request Router and Approver
| func validateReservedVolumeMounts(c *corev1.Container, base *field.Path, rules PodTemplateRules) field.ErrorList { | ||
| var errs field.ErrorList | ||
| for mi := range c.VolumeMounts { | ||
| if !isReservedVolumeMountName(c.VolumeMounts[mi].Name, rules.ReservedPrimaryVolumeMountNames) { |
There was a problem hiding this comment.
🔒 Agentic Security Review
Severity: HIGH
validateReservedVolumeMounts blocks sidecar/init mounts only by volume name, but it does not block user-defined pod volumes that reference operator-managed Secrets by secretName. A template author can define a new volume name that points at auth/TLS Secrets (for example signing-key secrets) and then mount that new volume in a sidecar.
Impact: users with template-write access can exfiltrate JWT signing private keys or other operator-managed secret material and forge credentials beyond intended secret-read boundaries.
Reviewed by Cursor Security Reviewer for commit b776dae. Configure here.
There was a problem hiding this comment.
Leaving this one open — it's the same policy question as the sidecar reserved-volume thread (#discussion_r3586622811, awaiting @fstr's call). Name-based blocking can't fully close it: a template author can reach an operator-owned Secret via a differently-named volume's secretName, or via env.valueFrom.secretKeyRef / envFrom / a projected or CSI volume. If we decide template-write should be lower-privilege than Secret-read, I'll add a check rejecting user volumes whose secretName targets an operator-owned Secret to narrow this; otherwise we accept it as risk. Deferring to that decision.
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, and Cursor Security Agent left unresolved findings (TLS downgrade window and secret volume secretName bypass). Reviewers gm42 and fstr are already assigned.
Sent by Cursor Approval Agent: Pull Request Router and Approver
| }, | ||
| wantField: "spec.template.metadata.finalizers", | ||
| }, | ||
| // A user-supplied sidecar or init container must not be able to mount the |
There was a problem hiding this comment.
Why shouldn't a sidecar be able to mount it?
There was a problem hiding this comment.
The block guards a specific privilege-escalation path, not sidecars in general. An engine-class template author needs only template-write access — not Kubernetes Secret-read RBAC — yet the operator renders the auth/TLS Secret volumes (auth-admin, auth-signing-<kid>, tls-engine) at pod level. Without this check, such an author could add a sidecar that mounts one of those volumes and read the JWT signing private keys or the admin password straight off disk — i.e. obtain credentials they can't otherwise kubectl get secret. That's the escalation being blocked.
Two honest caveats:
- The block matches by volume name only, so it's structurally incomplete — a template author can declare a new pod-level volume pointing at the same Secret via
secretName(and reach it other ways too:env.valueFrom.secretKeyRef,envFrom, projected/CSI volumes). This is exactly Cursor's open HIGH onoperatorauthority.go. Name-matching alone can't fully close it. - If engine-class template authors in our model are effectively cluster admins (who could read those Secrets anyway), the restriction is friction for no real gain.
So the real question is: do we treat template-write as a lower privilege than Secret-read? If yes, I'd keep the block and additionally reject user volumes whose secretName targets an operator-owned Secret to narrow (not fully close) the bypass. If no (template authors are trusted), I'll drop the block and mark the Cursor findings accepted-risk. Which matches our threat model?
|
(AI-assisted review) Review notes: PR #39 — auth/SSO, TLS, signing-key rotation (FB-896)Overall this is a well-designed and carefully documented change. The rotation The notes below are mostly about closing the gap between the design and what 1. Engine convergence lookup (worth addressing before merge)The rotation gates rely on With no engines matching the selector, the List comes back empty and the The unit tests pass because the fixture builder ( Suggested fix is small: enumerate engines the same way the watch mapper does 2. TLA+ coverageTwo places where the formal side could catch up with the code:
Neither has to block the merge, but a follow-up issue for the rotation spec 3. E2E coverage
4. Smaller items
|
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, Cursor Security Agent left an unresolved HIGH finding (secret volume secretName bypass), and fstr has requested changes. Reviewer gm42 is already assigned; assigning fstr for continued review.
Sent by Cursor Approval Agent: Infra Reviewer
**Summary** Phase 1 of FB-896: instance-wide authentication (native username/password plus OIDC bearer-token validation), rendered identically into every engine's instance.auth.* config and enforced by a validating webhook that mirrors packdb's AuthConfig::Validate() rules directly. - AuthSpec/LocalAuthSpec/OIDCAuthSpec CRD API on FireboltInstance, with admin/passwordLogin/signingKeys grouped under LocalAuthSpec and oidc/preferredAuthorizationServer at the top level. - cert-manager-only signing-key provisioning: one non-rotating Certificate per instance (PKCS8, auto-renew disabled since packdb reads keys only at startup); admin password is always user-supplied via Secret ref. - Engine propagation: InstanceInfo carries resolved auth + signing-key Secret names, buildConfigMap renders instance.auth.* per the closed schema, engine pods mount the signing-key and admin-password Secrets, and AnnotationAuthHash now hashes the full AuthSpec (not just Secret names) so config-only changes correctly trigger a new StatefulSet generation. - Lockdown: instance.auth added to OperatorOwnedEngineConfigPaths and the reserved engine volume-name set so customEngineConfig cannot override operator-managed auth. - Webhook validation closes gaps found by reading packdb's C++ source directly: provider names may not start with "_", preferredAuthorizationServer must name a configured server, and all packdb duration-string fields (tokenExpiry, refreshInterval, etc.) are now parsed and validated, including the positivity rule on discovery.refreshInterval. **Test Plan** - `make test`: all packages pass, internal/controller (envtest) at 76.5% coverage. - `make lint`: 0 issues. - New internal/controller/schema_conformance_test.go validates rendered config.yaml (disabled/native/OIDC) against a vendored copy of packdb's application-config.schema.json. - Cross-checked every semantic auth rule directly against packdb's src/PackDB/Auth/AuthConfig.cpp and confirmed cert-manager's PKCS8 encoding is accepted by SigningKeyManager.cpp. - Booting a real firebolt-core binary against the rendered config was intentionally skipped for this phase in favor of the static verification above.
…FB-896) **Summary** Phase 2 of FB-896, bundled with the gateway-upstream-reencrypt slice of Phase 3 (a hard technical requirement, not scope creep — see below): spec.tls.engine now provisions and enforces TLS on every engine's http-query listener. - Engine TLS is a full replace, not additive: packdb's EndpointConfig::ApplyToLegacyConfig wipes the plaintext http_port default the instant any endpoints.http.listeners entry is rendered, so enabling spec.tls.engine turns port 3473 into TLS-only rather than opening a second port. - cert-manager provisions one Certificate per instance with a namespace-wide wildcard SAN (*.<namespace>.svc.cluster.local, covering every engine's stable routing Service without tracking the live engine set) plus "localhost" for the web-UI sidecar's loopback connection. Readiness gates on ca.crt presence (not just tls.crt/tls.key): the gateway's trusted_ca needs it, so an issuer that never populates it (some ACME configs) correctly stays pending forever instead of silently breaking every gateway pod's config load. - Because plaintext disappears from port 3473, the gateway's dynamic_forward_proxy cluster gains an upstream transport_socket (Envoy UpstreamTlsContext) re-encrypting gateway->engine traffic, and the engine web UI sidecar's backend URL switches to https. Both are gated on the same Status.EngineTLS readiness as the engine side. - Envoy upstream TLS uses a static match_typed_subject_alt_names suffix matcher against the wildcard SAN, not auto_sni/auto_san_validation: the latter silently no-ops once typed_extension_protocol_options is configured on the cluster, and skips SAN validation on health-check connections entirely (health checks never traverse the HTTP router filter that populates it). Verified against Envoy's own source that transport_socket set on the parent dynamic_forward_proxy Cluster is inherited verbatim into every synthesized per-authority sub-cluster. - customEngineConfig can no longer override the rendered endpoints section (added to OperatorOwnedEngineConfigPaths), mirroring the auth lockdown from Phase 1. - Drift detection: AnnotationEngineTLSHash mirrors AnnotationAuthHash's reasoning (a Secret-name change behind an identically-named volume is invisible to VolumeMounts equality). The transport_socket's %s placeholder renders to nothing (not a blank line) when TLS is disabled, so contentHash(envoyYAML) stays identical for every existing non-TLS instance — no incidental gateway rollout on upgrade. - Confirmed directly against packdb source that the kubelet health probe port (8122) is bound from an independent health_check_port config key, untouched by endpoints.http/https rendering. **Known gaps, explicit not silent** - The live TLS handshake (cert-chain trust, SAN matching, health checks over TLS) is unverified: no shape/unit test can exercise it, and the e2e suite (which already installs cert-manager) was not run for this phase — accepted as a gap for now rather than assumed covered. - uiSidecar: true + engine TLS is a likely breakage: DefaultEngineWebImage is a separately-built nginx image this repo doesn't control, and whether its embedded config trusts an internal CA when proxying to https://localhost:3473 has not been checked against the real image. - spec.tls.gateway (downstream client->gateway termination) remains reserved, unwired CRD scaffolding — deliberately not validated by the webhook yet so it doesn't imply a feature that doesn't exist. **Test Plan** - `make build`/`make vet`/`make lint`: clean. - `make test`: all packages pass, internal/controller (envtest) at 76.2% coverage. - Schema-conformance test extended with engine-tls and auth-and-engine-tls fixtures against the vendored packdb schema. - New discriminating tests for: customEngineConfig endpoints lockdown, TLS-secret-name drift, ca.crt-required readiness gating, and transport_socket byte-identical rendering when TLS is disabled — each confirmed to fail without its corresponding fix via temporary revert.
…mes (FB-896) Cursor Security (high): user-supplied sidecars and init containers passed pod-template validation with their volumeMounts unchecked, while the operator renders the auth/TLS Secret volumes (auth-admin, auth-signing-<kid>, tls-engine) at pod level. A template author who cannot read those Secrets directly could mount one from a sidecar and read admin credentials or JWT signing keys off disk. validateContainersAgainstRules (sidecars) and validateInitContainersAgainstRules now reject any volumeMount naming an operator-owned volume, via isReservedVolumeMountName (covering the dynamic auth-signing-<kid> set by prefix) — matching the primary container's existing check and the model already documented on operatorOwnedEngineVolumeNames.
…eway (FB-896) Cursor Security (medium): the gateway mounted the whole engine TLS Secret to read ca.crt for upstream validation, needlessly exposing the engine listener's private key (tls.key) to the gateway pod. The engine-CA volume now projects only ca.crt via Items, so a gateway compromise cannot exfiltrate the engine's private key. Envoy already points solely at the ca.crt path, so the change is transparent.
Cursor Security (medium): EngineTLSReady/GatewayTLSReady were deliberately excluded from the Ready roll-up, so an Instance reported Ready=True while the gateway served plaintext on its client-facing listener during cert provisioning — a TLS downgrade window that advertised a secure posture it had not reached. setInstanceReadyRollup now includes EngineTLSReady and GatewayTLSReady. They report True/"Disabled" when their feature is off, so a non-TLS instance is unaffected; when TLS is requested the Instance holds Ready=False (reason CertificatePending) until the certificate issues. No deadlock: cert-manager issues certs independently and engines gate their own reconcile on Status.EngineTLS, not the top-level Ready. The TLA+ state/property harness materializes a TLS-disabled instance (conditions True/"Disabled") since the formal model does not track TLS.
… (FB-896) enginesConvergedOn listed engines with a MatchingLabels selector on firebolt.io/instance, but that label is only stamped on the operator's own child resources, never on the FireboltEngine CR (engines bind to their instance via spec.instanceRef). In a live cluster the List returned zero engines, so the promote / retain-anchor / remove rotation gates were vacuously 'converged' and advanced without waiting for engines to observe the new signing keys — re-opening the sign-vs-validate window the gating exists to close. Unit tests only passed because the fixture stamped a label real engines never carry. List all engines in the namespace and filter on spec.instanceRef, exactly as the instance->engine watch mapper (instanceToEngines) does; switch the engineWithHash fixture to set Spec.InstanceRef.
… (FB-896)
Flip the crypto defaults across the shared CertManagerSpec — so auth
signing keys and both engine/gateway TLS listeners now default to ECDSA
with the P-384 curve, and the embedded auth server's JWT signing
algorithm defaults to ES384. The three defaults are interdependent
(the RS* families require an RSA key, the ES* families an ECDSA key), so
they move together.
Also add a validateCertManagerKey admission check rejecting an
algorithm/size mismatch (RSA outside 2048..8192, ECDSA not in
{256,384,521}). kubebuilder applies the algorithm and size defaults
independently, so without this a partial override (e.g. algorithm=RSA
with the ECDSA-shaped default size) would pass admission and fail later
at cert-manager. The Go compatibility mirror is updated to the new
empty-value defaults in lockstep.
…FB-896) Add a SecretRef alternative to CertManager on TLSListenerSpec, so a TLS listener can consume a pre-issued Secret (tls.crt/tls.key, plus ca.crt on the engine listener) instead of provisioning a cert-manager Certificate — for certificates from a CA the cluster has no cert-manager integration with. Exactly one of certManager/secretRef is required when a listener is enabled (validated in validateTLSListener and re-run by the controller as defense-in-depth). When secretRef is set the controller provisions no Certificate and populates Status.*TLS from the referenced Secret once its required keys are present; it never creates, mutates, or GCs that Secret. Signing keys are intentionally NOT given a bring-your-own option — they stay cert-manager-only (SigningKeyPolicy). The now-stale doc comments that claimed TLS material is always cert-manager-provisioned are corrected.
…ow (FB-896) mTLS (D): add spec.tls.gateway.clientCASecretRef. When set, the gateway's client-facing listener requires a client certificate and verifies it against the referenced Secret's ca.crt (rendered as require_client_certificate + a validation_context, with the CA projected read-only into the Envoy pod). Status.GatewayTLS is gated on that Secret being present, so the listener and mount only appear once the file exists. Honored only for the gateway; setting it on spec.tls.engine is rejected at admission (engine-side verify-client is not wired yet). Fail closed (F): when gateway TLS is requested but the certificate has not been issued yet, omit the client-facing listener entirely instead of serving plaintext (buildFailClosedEnvoyConfigYAML). The client port then refuses connections rather than exposing cleartext, and Instance readiness stays gated on the GatewayTLSReady condition. Both probes move to /healthz on the always-plaintext metrics port: the client listener may be absent (fail-closed) or require a client cert the kubelet can't present (mTLS), so probing it would wedge readiness — while preStop's /healthcheck/fail still flips the process-wide health_check filter, preserving graceful drain.
1c4d69f to
492e8ef
Compare
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, Cursor Security Agent has an unresolved HIGH finding (secret volume secretName bypass), and fstr has requested changes. Reviewers gm42 and fstr are already assigned.
Sent by Cursor Approval Agent: Infra Reviewer
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, Cursor Security Agent left an unresolved HIGH finding (secret volume secretName bypass), and fstr has requested changes. Reviewers gm42 and fstr are already assigned.
Sent by Cursor Approval Agent: Infra Reviewer
950a659 to
492e8ef
Compare
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, Cursor Security Agent left an unresolved HIGH finding (secret volume secretName bypass), and fstr has requested changes. Reviewers gm42 and fstr are already assigned.
Sent by Cursor Approval Agent: Infra Reviewer
…ft, and webhook gaps (FB-896) Addresses the third review round (7×P1, 2×P2). All changes are unit-tested where the seam allows; cert-issuance and protocol-switch paths are e2e-only (envtest has no cert-manager CRDs). Engine TLS provisioning (P1, coupled, e2e-only): - #1 Per-generation engine serving certificates. The old instance-wide wildcard SAN (*.<ns>.svc.cluster.local) can never match packdb's two-label node hostnames, so TLS generations never became Ready. Each blue-green generation now gets its own cert-manager Certificate whose SANs cover that generation's pods (*.<gen-headless>.<ns>.svc.cluster.local — scale-safe), the routing Service, and localhost; node hosts render as full .svc.cluster.local FQDNs. The engine controller becomes a cert-manager consumer (new EnsureEngineTLSCert applied before the StatefulSet). The instance-wide cert narrows to a CA anchor: it supplies the issuer ca.crt the gateway trusts and the Status.EngineTLS signal; the gateway path is unchanged. - #2 CA chain to engine TLS. packdb reuses certificate_file as its own curl CA bundle, so a CA-signed leaf could not self-validate. The entrypoint now assembles a leaf+CA bundle at a writable path and points certificate_file at it. - Consequence: bring-your-own Secret (secretRef) is no longer supported on the ENGINE listener — a static cert cannot cover unbounded per-generation hostnames under packdb's forced hostname verification. Rejected at admission; gateway secretRef is unaffected. (Reverses the engine half of the prior round's topic-E.) Rollout coordination and drift (P1): - #3 Gate gateway re-encryption on engine-fleet convergence. New FireboltEngineStatus.ObservedEngineTLSHash (copied from the STS annotation in computeStable) and EngineTLSStatus.Reencrypting let the gateway switch to TLS only once every engine has rolled onto it, and retain the trust anchor through the drain on disable. Narrows — does not eliminate — the mixed-protocol rollout window (packdb is all-or-nothing). - #4 Roll engines when the admin Secret data changes. Its ResourceVersion is folded into authHash at both the engine-resolve and instance-convergence sites, so an in-place password rotation triggers a rollout. ResourceVersion, never a hash of the password bytes. - #5 Roll the gateway when its bring-your-own TLS Secret data changes. The Secret's ResourceVersion feeds the gateway configHash so an in-place cert rotation rolls the Deployment. Signing keys (P1): - #7 Support live signing-key algorithm/size migration. SigningKeyStatus now records the issued algorithm/size; an alg/size change mints a fresh named key via the rotation machinery (rotationPolicy:Never cannot regenerate in place). Webhook validation gaps: - #6 (P1) Enforce exact ECDSA curve↔JWT algorithm pairing (ES256↔P-256, ES384↔P-384, ES512↔P-521); RSA keeps its range check. - #8 (P2) Reject duplicate OIDC provider names (packdb's IssuerRegistry throws). - #9 (P2) Reject non-positive signing-key rotationInterval (perpetual churn).
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, Cursor Security Agent left an unresolved HIGH finding (secret volume secretName bypass), and fstr has requested changes. Reviewers gm42 and fstr are already assigned.
Sent by Cursor Approval Agent: Infra Reviewer
…teway roll, cert cleanup (FB-896) Addresses the fourth review round (3xP1, 4xP2). Four were regressions the round-3 commit introduced and shipped green because envtest has no cert-manager, so the per-generation-cert and gateway-hash paths went unexercised. - #1 (P1) signingAlgorithm and the signing key size are immutable after first set: CEL transition rules on LocalAuthSpec.SigningAlgorithm and (scoped to the signing path) SigningKeyPolicy.CertManager, re-enforced in the validating webhook for a clearer message. packdb exposes one global signing_algorithm and derives every key's curve from it, so a mixed-curve keyset is unrepresentable and in-place migration is unsound. The round-3 mint-on-drift (which produced exactly that invalid keyset) is removed. Supersedes round-3's live-migration decision. - #2 (P1) the fail-closed gateway rolls while its bring-your-own client-CA is still pending: gatewayTLSSecretVersions gates the client-CA on gatewayDownstreamTLSReady (matching the mount) and tolerates NotFound, instead of hard-erroring and aborting the whole gateway reconcile (which left the prior plaintext/one-way listener up — fail-open). - #3 (P1) auth disable preserves the monotonic SigningKeyGeneration counter, so re-enable mints a fresh generation instead of reusing leftover signing-1 key material (cert-manager reuses it behind the existing Secret name, resurrecting previously-issued tokens). - #4 (P2) per-generation engine TLS Certificates and their cert-manager-derived Secrets are swept on the same generation lifecycle as STS/Svc/CM (gcOrphanedResources) and on engine deletion (reconcileDelete), Certificate before Secret, tolerant of a missing cert-manager CRD (envtest). Corrects the false GC claim in ensureEngineTLSCert's doc. - #5 (P2) tlsHash folds the engine serving-cert key algorithm/size so a policy edit reissues the per-generation cert; the engine issuerRef is made immutable instead of folded, avoiding a cross-CA-anchor reissue mid-roll. - #6 (P2) the gateway rolls when the engine-CA anchor Secret is reissued in place: its ResourceVersion is folded into the gateway config hash while re-encryption is active. - #7 (P2) the client-ca gateway volume name is reserved (operatorOwnedGatewayVolumeNames), so a user sidecar cannot collide with it. No secret bytes are hashed anywhere (ResourceVersion / labels only), keeping the round-2 CodeQL dismissal valid. Claude-Session: https://claude.ai/code/session_01QCMwtCk3iXjQyJHGMJEF1q
…coverage (FB-896) The e2e suite never installed cert-manager and no spec exercised spec.auth / spec.tls, so the FB-896 auth/TLS feature (rounds 3-4) had zero end-to-end coverage — the gap that let four regressions ship green. This wires cert-manager into the suite and adds the first auth/TLS spec. - Register certmanagerv1 in the four in-process manager/client schemes, so the operator (run in-process as cluster-admin) can create Certificates. - InstallCertManager now waits on all three cert-manager deployments; new EnsureCAClusterIssuer / DeleteCAClusterIssuer stand up a CA-backed ClusterIssuer (SelfSigned root -> CA Certificate -> CA ClusterIssuer). A CA-backed issuer is required because engine TLS readiness gates on ca.crt. Wired into SynchronizedBeforeSuite (guarded/idempotent) and AfterSuite; no CI/Makefile change (cert-manager images pull from upstream). - Instance mutate hook (createInstanceWithMutate / SetupTestInstanceWithMutate) plus helpers to create the admin Secret, poll instance conditions, and verify the engine TLS listener from a client pod. - New auth_tls_test.go (Core): an auth + engine/gateway-TLS instance reaches Ready with AuthReady/EngineTLSReady/GatewayTLSReady True; the engine serves a per-generation certificate that curl verifies against the CA (proving the SAN and CA chain); an in-place signingAlgorithm change is rejected by the API server; and per-generation engine certs/secrets are reclaimed on engine deletion. Extended/Comprehensive coverage tiers are gated on this landing green in CI. Claude-Session: https://claude.ai/code/session_01QCMwtCk3iXjQyJHGMJEF1q
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, Cursor Security Agent left an unresolved HIGH finding (secret volume secretName bypass), and fstr has requested changes. Reviewers gm42 and fstr are already assigned.
Sent by Cursor Approval Agent: Infra Reviewer
…leanup (FB-896) reconcileDelete and gcOrphanedResources list cert-manager Certificates to reclaim per-generation engine TLS certs/secrets (round-4 #4). The list must tolerate not only a missing cert-manager CRD (IsNoMatchError, e.g. envtest) but also the Certificate type being absent from the client's scheme (IsNotRegisteredError) — as in fake-client unit tests that build a minimal scheme. The outerharness property test TestEngineOuterStateMachine failed intermittently (rapid only reaches the DeleteEngine -> Reconcile path under some seeds; the dev CI leg hit it, local/latest did not) with "no kind is registered for the type v1.CertificateList in scheme". Both errors mean no Certificates can exist for this client, so there is nothing to reclaim; in production the operator always registers certmanagerv1, so neither fires (and a genuinely missing registration would already have failed Certificate creation loudly). Fix: a shared certKindUnavailable(err) helper (IsNoMatchError || IsNotRegisteredError) used by both cleanup list guards. Claude-Session: https://claude.ai/code/session_01QCMwtCk3iXjQyJHGMJEF1q
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, Cursor Security Agent left an unresolved HIGH finding (secret volume secretName bypass), and fstr has requested changes. Reviewers gm42 and fstr are already assigned.
Sent by Cursor Approval Agent: Infra Reviewer
…n auth hash, cert-gen readiness, engine issuer CEL (FB-896) Address 5 real findings from the fifth review round (a sixth was a false alarm against the shipped CRDs). #1 is deferred as a documented known limitation per product decision. - #2 fail-closed gateway mTLS: clear Status.GatewayTLS when the client-CA Secret is pending so gatewayDownstreamTLSReady flips false and the fail-closed listener-omission path takes over, instead of leaving stale one-way/old-CA pods accepting clients the new policy should reject. - #3 fold each signing key's Secret ResourceVersion (never the key bytes) into authHash at both the render (resolveInstanceInfo) and convergence (enginesConvergedOn) sites, so a same-kid/same-name reissue rolls the fleet rather than leaving engines split across old/new key bytes. - #4 gate gateway/engine/signing readiness on the cert-manager Certificate being Ready for its CURRENT generation (per-condition ObservedGeneration), not just on the Secret carrying key material. A failed re-issuance keeps serving the still-valid old cert (status retained) but reports degraded — the deliberate opposite of the #2 fail-closed authorization control. - #5 freeze the engine TLS issuerRef via a field-scoped CEL transition rule on spec.tls.engine so it holds even when the validating webhook is disabled (the shipped Helm default); the webhook re-check is kept for a clearer message. Field-scoping leaves the gateway issuer mutable. - #1 (transient protocol-straddle 503s while toggling engine TLS on a live fleet) documented as a known limitation in examples/instance-auth-tls.yaml; the real fix is architectural (overlapping listeners) and deferred. Tests: new unit tests for each fix (incl. a mutation-verified stale-generation case) and a webhook-free envtest CEL suite. Regenerated CRDs via make manifests. Claude-Session: https://claude.ai/code/session_01QCMwtCk3iXjQyJHGMJEF1q
…signment (FB-896) Legit's generic-secret rule flagged instance_auth_test.go:602 (`AdminSecretVersion: probeAdmin.ResourceVersion`) by proximity to the "...SecretVersion" field name. The value is a Kubernetes Secret ResourceVersion (a monotonic counter), not a credential. Value-based suppression in .legitignore, matching the existing FB-896 test-fixture suppressions (inline `// legit:ignore-secrets` is not honored by this integration). Claude-Session: https://claude.ai/code/session_01QCMwtCk3iXjQyJHGMJEF1q
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, Cursor Security Agent left an unresolved HIGH finding (secret volume secretName bypass), and fstr has requested changes. Reviewers gm42 and fstr are already assigned.
Sent by Cursor Approval Agent: Infra Reviewer
The engine-TLS CEL test's listener-builder helper always received "ca-a" (the mutate hook, not the builder, sets "ca-b"), which golangci-lint's unparam linter flagged. Hardcode the issuer inside the (renamed) helper. Claude-Session: https://claude.ai/code/session_01QCMwtCk3iXjQyJHGMJEF1q
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, Cursor Security Agent left an unresolved HIGH finding (secret volume secretName bypass), and fstr has requested changes. Reviewers gm42 and fstr are already assigned.
Sent by Cursor Approval Agent: Infra Reviewer
… drift witness, dual-CA trust bundle (FB-896) Round-6 review raised four findings; implement the three that cluster as coordinated-rotation / staged-trust-transition work (#1, #3, #4). #2 (mutable TLS alg/size silently ignored under rotationPolicy:Never) remains a separate deferred fix. Shared primitive: gatewayRolloutComplete / gatewayServingCurrentConfig observe whether every gateway pod is actually serving the current config (pod-template config-hash match + rollout complete) — the signal #1 and #4 both gate on. The config-hash match is load-bearing: ensureGatewayTLS runs before the gateway Deployment is applied in a reconcile, so a plain rollout check would pass against the previous config. #1 Stage a fail-closed rollout before serving a tighter gateway client posture (plaintext->TLS, one-way->mTLS). A posture ordinal (recorded in the new GatewayTLSStatus.Mode) distinguishes a tightening transition from a loosening/steady one; on a tightening transition the operator withholds Status.GatewayTLS and serves fail-closed until gatewayServingCurrentConfig confirms the old, looser pods are gone, then serves secure. Closes the transient fail-open window (maxUnavailable=0 rolling update kept old insecure pods reachable behind the same Service). Loosening/steady transitions skip staging. #3 Witness cert-manager Certificate.Status.Revision on each signing key (SigningKeyStatus.ObservedCertRevision); an unexpected bump on an existing kid — a regeneration of the key material under a stable kid — is surfaced as a Warning event (new FireboltInstanceReconciler.EventRecorder). AuthReady stays true and the round-5 signing-Secret-ResourceVersion-in-authHash convergence roll is unchanged: a fresh-kid rebuild would be slower to a consistent active key and cannot recover tokens a destructive regeneration has already destroyed. Observability only. #4 Mount an operator-assembled engine trust BUNDLE (ensureEngineCABundle): the union of every live generation's CA plus the instance anchor, deduped and self-pruning as generations retire, so the gateway keeps trusting engines across a CA rotation behind the (name-immutable) issuer. The engine's blue-green cutover (the Service-selector flip in computeCreating) is gated on the generation's CA fingerprint appearing in the gateway's confirmed-rolled set (Status.RolledEngineTrustCAs, published only once gatewayServingCurrentConfig holds). Race-free and non-over-blocking: same-CA rollouts find their CA already trusted and never wait; only a genuine CA rotation does. The gate is vacuous until the gateway actually re-encrypts upstream (engineUpstreamTLSReady) — a must, or the initial enable ramp would deadlock (info.TLS is set once the anchor is ready, but the trust bundle is only published once the gateway re-encrypts, which requires the fleet to cut over first). Local: go build/vet, go test ./... (all packages), the outerharness property test, golangci-lint (0 issues), and make manifests + CRD conformance all green. New CRD status fields: gatewayTLS.mode, auth.signingKeys[].observedCertRevision, rolledEngineTrustCAs. Claude-Session: https://claude.ai/code/session_01QCMwtCk3iXjQyJHGMJEF1q
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, Cursor Security Agent left an unresolved HIGH finding (secret volume secretName bypass), and fstr has requested changes. Reviewers gm42 and fstr are already assigned.
Sent by Cursor Approval Agent: Infra Reviewer
…ecycle (FB-896) Round-7 review raised six findings; implement all six. Five refine the round-6 coordinated-rotation work (5ab49a6); #4 is a pre-existing readiness-accuracy gap that round-6 put in sharper relief. They cluster into three subsystems. Gateway posture transition (#1, #2, #5): #1 Close the fail-open window on a posture tighten instead of relocating it. Round-6 staged fail-closed but the looser->fail-closed roll was itself a maxUnavailable=0 rollout, so old looser pods kept serving behind the one client-facing Service selector throughout the roll. gatewayRollingUpdateStrategy now drops old pods to zero endpoints first (maxUnavailable=100%, maxSurge=0) whenever the gateway is fail-closed pending a tighter posture (gatewayDownstreamTLSPending) — a brief reject-all in exchange for never being fail-open. Steady state and the fail-closed->secure roll keep the zero-downtime default. Fully eliminating the window (no reject-all AND no fail-open) still needs the deferred dual-listener architecture. #2 Treat a client-CA replacement as a tightening transition. A swap CA-A->CA-B keeps the posture ordinal at MutualTLS on both sides, so it previously rolled without staging, leaving old pods trusting the retired CA-A during the roll. Record the served client CA's fingerprint (new GatewayTLSStatus.ClientCAFingerprint, a SHA-256 of the public ca.crt) and stage fail-closed when it changes. A blank recorded fingerprint (a status written before this field existed) is recorded rather than restaged, so an operator upgrade does not force a one-time reject-all on every already-serving mTLS gateway. #5 Report GatewayTLSReady only once the secure config is actually serving. ensureGatewayTLS runs before the Deployment is applied, so it used to flip Ready as soon as the certificate was ready — while the still-Ready fail-closed pods (metrics-port probe) kept GatewayReady>0 and the client port still rejected; with an invalid BYO certificate that never loads, forever. Gate the condition on gatewayServingCurrentConfig (reason SecureRolloutPending). Scope the check to the transition out of a not-ready state so a later zero-downtime renewal does not flap the Instance out of Ready; a genuine re-tighten resets the condition and re-gates. Engine CA trust bundle (#3, #6): #3 Stop pinning the retired anchor CA. ensureEngineCABundle assembled the union of live per-generation CAs plus the instance anchor, but the anchor is rotationPolicy:Never with a ~100yr lifetime, so its ca.crt stays pinned to the CA that signed it and a retired CA never pruned out after its generations drained. Assemble from live per-generation CAs only (the certs engines actually serve), keeping the anchor solely as a fallback when no per-generation CA is discoverable so the bundle is never emptied out from under a re-encrypting fleet. #6 Preserve confirmed trust state on a bundle-assembly error. A failed ensureEngineCABundle returned nil fingerprints, which were still published, clobbering the last-confirmed Status.RolledEngineTrustCAs and wrongly blocking engine cutovers the gateway could still serve. The guard now lives in publishRolledEngineTrustCAs (bundleErr parameter): on error it preserves the prior set. The clear-on-disable path is unaffected (assembly returns no error then). Readiness semantics (#4): Split the engine-TLS signal so the Instance does not advertise Ready over a still-plaintext gateway->engine hop. EngineTLSReady is now convergence-gated (True only once the fleet is re-encrypting; False/Converging while provisioned but not yet converged) and feeds the Ready roll-up. The engine roll that produces that convergence is unblocked by the provisioned fact (Status.EngineTLS != nil) in resolveInstanceInfo, NOT by the condition — gating the roll on the now-convergence- gated condition would deadlock the enable ramp (engines never roll -> never converge -> condition never True). A stopped engine still converges via a trivial zero-replica roll, so the Converging state is transient. New CRD status field: gatewayTLS.clientCAFingerprint. Tests cover all six findings (including the #5 renewal anti-flap and the #4 no-deadlock regression) and update the round-6 staged tests to the new three-phase readiness timing. Local: go build/vet, golangci-lint (0 issues), go test ./... (all packages), the outerharness property path, and make manifests + CRD conformance all green. Claude-Session: https://claude.ai/code/session_01QCMwtCk3iXjQyJHGMJEF1q
There was a problem hiding this comment.
Stale comment
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, Cursor Security Agent left an unresolved HIGH finding (secret volume secretName bypass), and fstr has requested changes. Reviewers gm42 and fstr are already assigned.
Sent by Cursor Approval Agent: Infra Reviewer
…tion/UX fixes (FB-896) Round-8 review raised six findings; implement all six. Several hinge on cert-manager rotationPolicy:Never, under which a re-issuance reuses the private key and only re-writes the certificate — bumping Certificate.Status.Revision and the Secret's ResourceVersion without changing key material. Both packdb-dependent findings (#3, #5) were confirmed against firebolt-db/packdb source. Correctness core (#1, #2, #4): #1 Roll engine pods when their per-generation serving certificate is re-issued. The engine drift hash is generation-independent (anchor Secret name + key policy), so a cert-manager re-issuance of the leaf packdb loaded at startup was invisible: running pods kept serving stale material until an unrelated restart — eventually an expired cert, or a leaf a rotated CA's refreshed trust bundle no longer accepts. Latent under the ~100yr duration, but reached by any issuer that caps the lifetime (Vault/PCA/ACME) or a manual `cmctl renew`. The signal cannot ride tlsHash (the instance side reconstructs that hash with no generation context, so folding a per-generation value would deadlock the re-encryption convergence gate); instead resolveInstanceInfo reads the current generation's serving-cert fingerprint live into ResolvedEngineTLSInfo.ServingCertFP, and computeStable rolls a fresh blue-green generation — coordinated through the existing trust-bundle cutover gate — when it differs from the fingerprint recorded for that SAME generation (FireboltEngineStatus.ObservedEngineServingCert FP/Gen). The generation guard is essential: every new generation issues its own fresh-key certificate, so its fingerprint legitimately differs from the prior one; comparing across a generation boundary would read every ordinary spec-driven roll as a re-issuance and roll forever. #2 Freeze engine and gateway TLS key algorithm/size while the listener stays enabled. The stable-name anchor and gateway certificates reuse their existing key under rotationPolicy:Never, so an in-place algorithm/size edit cannot regenerate the key to match and wedges the Certificate. Field-scoped CEL transition rules on TLSSpec.Engine/Gateway plus a webhook re-check (validateImmutableTLSKeyParams) reject it, mirroring the existing signing-key and engine-issuerRef immutability; a disable/re-enable (fresh key material) is still permitted, so the round-7 per-generation reissue path stays meaningful on first-enable and re-enable. #4 Detect signing-key identity rather than treating every certificate revision as a key regeneration. Under rotationPolicy:Never a cert-only reissuance (an issuer-capped lifetime or a manual renewal) bumps Status.Revision while reusing the key, so the previous revision-based witness raised a false "tokens can no longer be validated" warning and the ResourceVersion folded into authHash forced an unnecessary fleet rollout. Both now key off the public-key fingerprint (SHA-256 of the SubjectPublicKeyInfo parsed from tls.crt — the public key, never the private bytes): the SigningKeyMaterialRegenerated warning fires only when the public key actually changes, and authHash folds the fingerprint at both mirror sites (resolveInstanceInfo and enginesConvergedOn) in place of the ResourceVersion. A cert-only reissuance no longer rolls the fleet; a genuine key replacement still does. Because authHash's value changes, every auth-enabled instance performs one blue-green engine roll when this operator upgrade lands. Validation / UX (#3, #5, #6): #3 Include clock-skew tolerance in the signing-key retention floor. packdb accepts a token until iat + maxTokenAge + clock_skew_tolerance (specs/ authentication.md; default 30s), so the retained key must outlive that whole window. validateSigningKeyRotation now requires retainDuration >= maxTokenAge + the resolved local clock-skew tolerance (packdbDefaultClockSkew when unset; a bad value falls back to the default rather than lowering the floor). #5 Accept the multi-component day durations packdb accepts. packdb's duration parser (Duration.cpp) is a component tokenizer that allows "d" (days) combined with the standard units in any order, e.g. "1d12h"; the webhook previously accepted only Go grammar or a single "<num>d" suffix and over-rejected valid config at admission. parsePackdbDuration now tokenizes components faithfully (fast-pathing standard strings through time.ParseDuration). #6 Report the correct scheme for TLS-enabled port-forwards. The kubectl-firebolt gateway and engine port-forward commands printed http://localhost unconditionally, but each reaches a listener that terminates TLS on the same forwarded port when enabled. The commands now inspect the referenced Instance (gateway TLS directly, engine TLS via spec.instanceRef) and print https://, degrading to a protocol-neutral endpoint if the spec cannot be read; the --print-commands path is unaffected. New CRD status fields: fireboltengine observedEngineServingCertFP/Gen; the signing-key witness moves to observedPublicKeyFingerprint (a pointer, keeping SigningKeyStatus compact). New CEL rules on spec.tls.engine/gateway.certManager. Tests cover all six, including the #4 cert-only-reissuance-does-not-warn/roll distinction and a #1 cross-generation regression proving a freshly-rolled generation is not mistaken for an in-place re-issuance. Local: golangci-lint (0 issues), go test ./... , the outerharness property path, and make manifests + CRD conformance all green. Claude-Session: https://claude.ai/code/session_01QCMwtCk3iXjQyJHGMJEF1q
There was a problem hiding this comment.
Risk: high. Not approving: this auth/SSO/TLS/signing-key change exceeds the medium risk threshold, Cursor Security Agent left an unresolved HIGH finding (secret volume secretName bypass), and fstr has requested changes. Reviewers gm42 and fstr are already assigned.
Sent by Cursor Approval Agent: Infra Reviewer





Summary
Implements FB-896: engine-side authentication/SSO, full TLS (engine-listener + gateway upstream/downstream), and operator-owned JWT signing-key rotation.
8daa31c):spec.authonFireboltInstance— native/OIDC authorization servers, operator-provisioned admin credentials wiring and JWT signing key via cert-manager, rendered into every engine'sinstance.auth.*per packdb's closed config schema.12d6e17):spec.tls.engineterminates TLS on each engine's query listener; the gateway re-encrypts its upstream connection to engines using the same CA.4292941):spec.tls.gatewayterminates TLS on the gateway's client-facing Envoy listener, with matching probe-scheme and cert provisioning.e8dcaf0): optionalsigningKeys.rotationInterval/retainDurationdrive a two-phase mint → promote → retain → remove state machine, gated at every step on a newFireboltEngineStatus.ObservedAuthHashfield so a key is never promoted or removed until every engine has actually converged on the config that makes that step safe. The retain window is anchored at confirmed post-promotion convergence rather than the promotion decision, closing a validation-gap window a naive decision-time anchor would reopen.All crypto material (signing keys, TLS certs) is provisioned via cert-manager
Certificates against a user-suppliedIssuer/ClusterIssuer— no bring-your-own-secret option, matching this operator's existing TLS design.Known limitations (carried forward, not new)
firebolt-coreengine — covered by unit/golden-file tests and cert-manager-backed e2e infra only for the apply/issue path.DefaultEngineWebImage's CA-trust behavior when proxying tohttps://localhost:3473withuiSidecar: true+ engine TLS enabled is unverified against the real image.Test plan
go build ./...,go vet ./...,go test ./...all cleanmake lintclean (0 issues)make manifests generatere-run confirmed idempotentNote
High Risk
Large CRD and behavioral change across authentication, TLS termination, and coordinated JWT signing-key rotation; misconfiguration or rotation bugs could break logins, cross-engine token validation, or TLS paths before engines start.
Overview
Replaces the placeholder
spec.auth(mode+ OAuth client-style OIDC) with an instance-wide, packdb-shaped auth model:enabled, embedded local login (admin password via Secret mount, cert-manager JWT signing keys), and multi-provider OIDC bearer validation (discovery URL, username mapping, JIT, JWKS/discovery TTLs). Addsspec.tlsfor gateway client-facing and engine listeners, with certificates only via cert-managerIssuerRef(no BYO Secret).Status and rollout:
status.authtracks signing keys and a rotation state machine (Active/ValidationOnly/Removing,RetireEligibleAt); optionalrotationInterval/retainDurationare validated at admission.FireboltEngine.status.observedAuthHashexposes the stable auth config hash so the instance controller can gate promote/retire on every engine converging. NewAuthReady,EngineTLSReady, andGatewayTLSReadyconditions are documented as not folded into top-level Instance Ready.Operator surface: Reserved engine/gateway volumes for admin creds, per-kid signing secrets (
auth-signing-*prefix), and TLS;instance.authandendpointsjoin operator-owned engine config paths. Webhook addsValidateAuth/ValidateTLS(re-runnable at reconcile), packdb duration parsing (1detc.), and broad unit tests. Runtime wiring: cert-manager API on the scheme, Certificate RBAC, dependency and regenerated CRD/Helm JSON schema updates, and an expandedinstance-fullexample.Reviewed by Cursor Bugbot for commit e8dcaf0. Configure here.