Skip to content

feat: TLS and env-based endpoint for jumpstarter-telemetry - #975

Open
bkhizgiy wants to merge 1 commit into
jumpstarter-dev:mainfrom
bkhizgiy:tls_logs
Open

feat: TLS and env-based endpoint for jumpstarter-telemetry#975
bkhizgiy wants to merge 1 commit into
jumpstarter-dev:mainfrom
bkhizgiy:tls_logs

Conversation

@bkhizgiy

@bkhizgiy bkhizgiy commented Aug 5, 2026

Copy link
Copy Markdown
Member

Follow-up to #930

jumpstarter-telemetry now always uses TLS.
Set EXTERNAL_CERT_PEM and EXTERNAL_KEY_PEM to mount operator-provided certs,
falls back to a self-signed cert when absent.
The controller now reads GRPC_TELEMETRY_ENDPOINT from the
environment instead of auto-deriving jumpstarter-telemetry.<ns>:9093
from the ConfigMap loader.

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 5373caf2-bcaa-405d-b489-f5554c8c3763

📥 Commits

Reviewing files that changed from the base of the PR and between c18ac85 and 477b094.

📒 Files selected for processing (7)
  • controller/cmd/telemetry/main.go
  • controller/internal/config/config.go
  • controller/internal/config/types.go
  • controller/internal/service/controller_service.go
  • controller/internal/service/endpoints.go
  • controller/internal/service/telemetry_service.go
  • controller/internal/service/telemetry_service_test.go
💤 Files with no reviewable changes (1)
  • controller/internal/config/config.go
🚧 Files skipped from review as they are similar to previous changes (6)
  • controller/internal/config/types.go
  • controller/internal/service/controller_service.go
  • controller/internal/service/endpoints.go
  • controller/cmd/telemetry/main.go
  • controller/internal/service/telemetry_service.go
  • controller/internal/service/telemetry_service_test.go

📝 Walkthrough

Walkthrough

The telemetry service now uses an explicit or environment-derived endpoint. It always starts with TLS, using external PEM files when configured or a self-signed certificate with endpoint-derived SANs. Tests cover endpoint precedence and TLS credential failures.

Changes

Telemetry service

Layer / File(s) Summary
Telemetry endpoint discovery
controller/internal/config/config.go, controller/internal/config/types.go, controller/internal/service/controller_service.go, controller/internal/service/endpoints.go, controller/internal/service/telemetry_service_test.go
Telemetry configuration no longer derives a namespace-based endpoint. Endpoint responses use the configured value or GRPC_TELEMETRY_ENDPOINT. Tests cover fallback, empty values, and precedence.
TLS credential loading and startup
controller/internal/service/telemetry_service.go, controller/cmd/telemetry/main.go, controller/internal/service/telemetry_service_test.go
Startup loads external certificates or generates a self-signed certificate with endpoint-derived SANs. The gRPC server uses TLS credentials. Tests cover valid, invalid, missing, and fallback credentials.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ControllerService
  participant TelemetryService
  participant CertificateFiles
  participant GRPCServer
  ControllerService->>TelemetryService: Advertise configured or environment endpoint
  TelemetryService->>CertificateFiles: Load external PEM credentials
  alt External credentials unavailable
    TelemetryService->>TelemetryService: Generate self-signed certificate
  end
  TelemetryService->>GRPCServer: Start with TLS credentials
Loading

Possibly related PRs

Poem

I’m a rabbit with TLS in my burrow tonight,
Certificates guard every telemetry flight.
If PEM files are absent, SANs bloom in the air,
And endpoint paths guide the self-signed pair.
Config wins first; environment comes through—
Secure telemetry hops safely to you.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.75% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main changes: mandatory TLS and environment-based telemetry endpoint configuration.
Description check ✅ Passed The description directly explains the TLS behavior, certificate configuration, self-signed fallback, and environment-based endpoint changes.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 golangci-lint (2.12.2)

level=error msg="[linters_context] typechecking error: pattern ./...: directory prefix . does not contain main module or its selected dependencies"


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@bkhizgiy

bkhizgiy commented Aug 5, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/internal/config/types.go`:
- Around line 38-43: Update the Telemetry.Certificate documentation comment to
remove the claim that an empty value enables system CA verification for
self-signed certificates. State that self-signed mode requires an explicit
unverified-client policy, or that the operator must provide a stable CA
certificate and configure it for verification.

In `@controller/internal/service/controller_service.go`:
- Around line 330-334: The controller and telemetry server resolve different
endpoint sources, causing advertised endpoints to disagree with certificate
SANs. In controller/internal/service/controller_service.go:330-334,
controller/internal/service/telemetry_service.go:226-240, and
controller/cmd/telemetry/main.go:24-25, introduce and use one shared endpoint
configuration for both processes, derive self-signed SANs from that resolved
value, and advertise only that value; add an integration test verifying a TLS
client validates the certificate for the endpoint returned by
GetServiceEndpoints.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a1796af-2663-4c0b-9759-2c325f7c694a

📥 Commits

Reviewing files that changed from the base of the PR and between c18ac85 and 201e180.

📒 Files selected for processing (7)
  • controller/cmd/telemetry/main.go
  • controller/internal/config/config.go
  • controller/internal/config/types.go
  • controller/internal/service/controller_service.go
  • controller/internal/service/endpoints.go
  • controller/internal/service/telemetry_service.go
  • controller/internal/service/telemetry_service_test.go
💤 Files with no reviewable changes (1)
  • controller/internal/config/config.go

Comment thread controller/internal/config/types.go Outdated
Comment thread controller/internal/service/controller_service.go
Signed-off-by: Bella Khizgiyaev <bkhizgiy@redhat.com>
Assisted-by: Claude Sonnet 4.5 <claude@anthropic.com>
@bkhizgiy
bkhizgiy marked this pull request as ready for review August 5, 2026 13:42
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Note

GitHub couldn't provide a complete incremental comparison for this pull request, so CodeRabbit is performing a full review instead. This review may take a little longer.

@bkhizgiy
bkhizgiy requested review from bennyz and raballew August 6, 2026 07:31
Comment on lines 329 to 337
if s.TelemetryConfig != nil && s.TelemetryConfig.Enabled {
// Prefer the explicit ConfigMap endpoint; fall back to GRPC_TELEMETRY_ENDPOINT
// so the operator can pass the address via env var without touching the ConfigMap.
ep := cmp.Or(s.TelemetryConfig.Endpoint, telemetryEndpoint())
resp.TelemetryEndpoints = append(resp.TelemetryEndpoints, &pb.TelemetryEndpoint{
Endpoint: s.TelemetryConfig.Endpoint,
Endpoint: ep,
Certificate: s.TelemetryConfig.Certificate,
MinSeverity: cmp.Or(s.TelemetryConfig.Logging.Filter.MinSeverity, "info"),
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When GRPC_TELEMETRY_ENDPOINT is unset and TelemetryConfig.Endpoint is also empty, cmp.Or("", "") returns "". The append highlighted here, runs unconditionally, so all exporters receive a TelemetryEndpoint{Endpoint: ""} and attempt to dial an empty string. I think you need to guard the append or return error.

}

srv := grpc.NewServer()
srv := grpc.NewServer(grpc.Creds(creds))

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The errCh branch (case err := <-errCh: return err) in the select block below returns without stopping the gRPC server.

Comment on lines +212 to +260
certPEMPath := os.Getenv("EXTERNAL_CERT_PEM")
keyPEMPath := os.Getenv("EXTERNAL_KEY_PEM")

var cert *tls.Certificate
if certPEMPath != "" && keyPEMPath != "" {
certPEMBytes, readErr := os.ReadFile(certPEMPath)
if readErr != nil {
return nil, "", fmt.Errorf("failed to read external certificate file: %w", readErr)
}
keyPEMBytes, readErr := os.ReadFile(keyPEMPath)
if readErr != nil {
return nil, "", fmt.Errorf("failed to read external key file: %w", readErr)
}
parsedCert, parseErr := tls.X509KeyPair(certPEMBytes, keyPEMBytes)
if parseErr != nil {
return nil, "", fmt.Errorf("failed to parse external certificate: %w", parseErr)
}
cert = &parsedCert
} else {
// Derive the TLS SAN from the advertised endpoint (what clients connect to),
// not from the bind address (which is a local port like ":9093").
// Same pattern as the router and controller services.
// IMPORTANT: GRPC_TELEMETRY_ENDPOINT must be set on the telemetry pod itself
// so the SAN matches the endpoint the controller advertises to exporters.
advertised := telemetryEndpoint()
var dnsnames []string
var ipaddresses []net.IP
if advertised != "" {
var sanErr error
dnsnames, ipaddresses, sanErr = endpointToSAN(advertised)
if sanErr != nil {
dnsnames = []string{"localhost"}
}
} else {
// No advertised endpoint configured — development/local mode.
dnsnames = []string{"localhost"}
}
var genErr error
cert, genErr = NewSelfSignedCertificate("jumpstarter telemetry", dnsnames, ipaddresses)
if genErr != nil {
return nil, "", genErr
}
// Encode the leaf cert as PEM so the caller can log it for the operator.
selfSignedPEM = string(pem.EncodeToMemory(&pem.Block{
Type: "CERTIFICATE",
Bytes: cert.Certificate[0],
}))
}
return credentials.NewServerTLSFromCert(cert), selfSignedPEM, nil

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TLS credential loading is duplicated between this block and controller_service.go:1206-1228 and I think they already diverged.

Comment on lines +271 to 286
if selfSignedPEM != "" {
// Log the self-signed cert so the operator can copy it into the controller
// ConfigMap's telemetry.certificate field. Exporters need this PEM to verify
// the TLS connection — a self-signed cert is not trusted by the system CA pool.
logger.Info("Using self-signed TLS certificate; copy certPEM into the controller ConfigMap telemetry.certificate so exporters can verify TLS",
"certPEM", selfSignedPEM)
}

lis, err := net.Listen("tcp", s.BindAddr)
if err != nil {
return fmt.Errorf("telemetry: listen %s: %w", s.BindAddr, err)
}

srv := grpc.NewServer()
srv := grpc.NewServer(grpc.Creds(creds))
pb.RegisterTelemetryServiceServer(srv, s)
reflection.Register(srv)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The core behavioral change introduced by this PR, grpc.NewServer(grpc.Creds(creds)) mandating TLS for all connections, has no behavioral test.

Comment on lines +97 to 105
ep := cfg.Endpoint
if ep == "" {
ep = telemetryEndpoint()
}
resp.TelemetryEndpoints = append(resp.TelemetryEndpoints, &pb.TelemetryEndpoint{
Endpoint: cfg.Endpoint,
Endpoint: ep,
Certificate: cfg.Certificate,
MinSeverity: minSev,
})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The production line ep := cmp.Or(s.TelemetryConfig.Endpoint, telemetryEndpoint()) in controller_service.go has zero coverage.

Comment on lines 127 to 131
if err := config.Telemetry.Validate(); err != nil {
return nil, err
}
// Auto-derive the gRPC address when the operator has not overridden it.
// The well-known service name follows the same pattern as the controller
// and router: <service>.<namespace>.svc (in-cluster DNS).
if config.Telemetry.Endpoint == "" {
config.Telemetry.Endpoint = "jumpstarter-telemetry." + key.Namespace + ":9093"
}
telemetry = config.Telemetry
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

GRPC_TELEMETRY_ENDPOINT is read in the service layer (endpoints.go) rather than here in LoadConfiguration. As a result, LoadedConfig.Telemetry.Endpoint is always "" when the ConfigMap omits the field, even if the env var is set.

Comment on lines +24 to +26
func telemetryEndpoint() string {
return os.Getenv("GRPC_TELEMETRY_ENDPOINT")
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The raw env var value is returned with no host:port format validation before it is distributed to all exporters.

Bytes: cert.Certificate[0],
}))
}
return credentials.NewServerTLSFromCert(cert), selfSignedPEM, nil

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Whats our required TLS min version?

Comment on lines 279 to 282
lis, err := net.Listen("tcp", s.BindAddr)
if err != nil {
return fmt.Errorf("telemetry: listen %s: %w", s.BindAddr, err)
}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After net.Listen succeeds there is no defer lis.Close() guard.

Comment on lines +27 to 35
// Endpoint: GRPC_TELEMETRY_ENDPOINT must be set on BOTH this pod and the controller
// pod to the same value (e.g. "jumpstarter-telemetry.jumpstarter.svc:9093").
// The telemetry service uses it to generate the correct SAN in the self-signed
// certificate; the controller uses it to advertise the address to exporters via
// GetServiceEndpoints. A mismatch causes TLS hostname verification failures.
//
// Future phases will add direct Loki push and MetricsStream for reverse-scrape
// of exporter prometheus_client registries.
package main

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the main() function below, the goroutine calling svc.Start(ctx) is launched before signal.Notify is registered. In the narrow window between goroutine start and signal subscription, SIGINT and SIGTERM use Go's default disposition (immediate process termination). A signal arriving in this window bypasses the graceful shutdown path entirely.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants