-
Notifications
You must be signed in to change notification settings - Fork 33
feat: TLS and env-based endpoint for jumpstarter-telemetry #975
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -127,12 +127,6 @@ func LoadConfiguration( | |
| 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 | ||
| } | ||
|
Comment on lines
127
to
131
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -327,8 +327,11 @@ func (s *ControllerService) GetServiceEndpoints( | |
| resp := &pb.GetServiceEndpointsResponse{} | ||
|
|
||
| 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, | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| Certificate: s.TelemetryConfig.Certificate, | ||
| MinSeverity: cmp.Or(s.TelemetryConfig.Logging.Filter.MinSeverity, "info"), | ||
| }) | ||
|
Comment on lines
329
to
337
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. When |
||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -21,6 +21,10 @@ func routerEndpoint() string { | |
| return ep | ||
| } | ||
|
|
||
| func telemetryEndpoint() string { | ||
| return os.Getenv("GRPC_TELEMETRY_ENDPOINT") | ||
| } | ||
|
Comment on lines
+24
to
+26
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The raw env var value is returned with no |
||
|
|
||
| func endpointToSAN(endpoint string) ([]string, []net.IP, error) { | ||
| host, _, err := net.SplitHostPort(endpoint) | ||
| if err != nil { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -18,9 +18,12 @@ package service | |
|
|
||
| import ( | ||
| "context" | ||
| "crypto/tls" | ||
| "encoding/pem" | ||
| "errors" | ||
| "fmt" | ||
| "net" | ||
| "os" | ||
| "strings" | ||
| "time" | ||
|
|
||
|
|
@@ -29,6 +32,7 @@ import ( | |
| pb "github.com/jumpstarter-dev/jumpstarter/controller/internal/protocol/jumpstarter/v1" | ||
| "google.golang.org/grpc" | ||
| "google.golang.org/grpc/codes" | ||
| "google.golang.org/grpc/credentials" | ||
| "google.golang.org/grpc/reflection" | ||
| "google.golang.org/grpc/status" | ||
| ctrl "sigs.k8s.io/controller-runtime" | ||
|
|
@@ -55,12 +59,11 @@ var reservedExtraFieldKeys = map[string]struct{}{ | |
| // TelemetryService receives structured log entries from exporters and clients, | ||
| // logs them via structured stdout, and will forward them to Loki in a future phase. | ||
| // | ||
| // Phase 1 design: the server listens on plaintext gRPC only. TLS termination is | ||
| // expected to be handled by a sidecar (e.g. Envoy) or service mesh in production | ||
| // deployments. The Certificate field advertised via GetServiceEndpoints is reserved | ||
| // for a future phase where the telemetry binary manages its own TLS credentials. | ||
| // Do NOT configure the Certificate field in the ConfigMap for Phase 1 deployments — | ||
| // exporters that receive a certificate will attempt a TLS handshake that will fail. | ||
| // TLS: the server always uses TLS. When EXTERNAL_CERT_PEM and EXTERNAL_KEY_PEM | ||
| // env vars point to certificate/key files (mounted by the operator from a Secret), | ||
| // those are loaded. Otherwise a self-signed certificate is generated — traffic is | ||
| // still encrypted, but clients cannot verify the server identity without the CA cert | ||
| // in the ConfigMap telemetry.certificate field. | ||
| type TelemetryService struct { | ||
| pb.UnimplementedTelemetryServiceServer | ||
|
|
||
|
|
@@ -197,16 +200,88 @@ func truncate(s string, n int) string { | |
| return s[:b] | ||
| } | ||
|
|
||
| // loadTLSCredentials loads TLS credentials for the gRPC server. | ||
| // It reads EXTERNAL_CERT_PEM and EXTERNAL_KEY_PEM env vars (file paths set by the | ||
| // operator via Secret volume mounts). When either is absent it falls back to a | ||
| // self-signed certificate so that traffic is always encrypted. | ||
| // | ||
| // selfSignedPEM is non-empty only when a self-signed certificate was generated. | ||
| // Callers should log it so the operator can copy it into the ConfigMap's | ||
| // telemetry.certificate field — exporters need this PEM to verify the TLS connection. | ||
| func (s *TelemetryService) loadTLSCredentials() (creds credentials.TransportCredentials, selfSignedPEM string, err error) { | ||
| 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) | ||
|
Comment on lines
+221
to
+223
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we have a test for valid cert but missing/invalid key? |
||
| } | ||
| 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 | ||
|
Comment on lines
+212
to
+260
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. TLS credential loading is duplicated between this block and
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Whats our required TLS min version? |
||
| } | ||
|
|
||
| // Start starts the TelemetryService gRPC server and blocks until ctx is cancelled. | ||
| func (s *TelemetryService) Start(ctx context.Context) error { | ||
| logger := ctrl.Log.WithName("telemetry").WithValues("component", "telemetry") | ||
|
|
||
| creds, selfSignedPEM, err := s.loadTLSCredentials() | ||
| if err != nil { | ||
| return fmt.Errorf("telemetry: load TLS credentials: %w", err) | ||
| } | ||
| 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) | ||
| } | ||
|
Comment on lines
279
to
282
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. After |
||
|
|
||
| srv := grpc.NewServer() | ||
| srv := grpc.NewServer(grpc.Creds(creds)) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The |
||
| pb.RegisterTelemetryServiceServer(srv, s) | ||
| reflection.Register(srv) | ||
|
Comment on lines
+271
to
286
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The core behavioral change introduced by this PR, |
||
|
|
||
|
|
||
There was a problem hiding this comment.
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 callingsvc.Start(ctx)is launched beforesignal.Notifyis 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.