-
Notifications
You must be signed in to change notification settings - Fork 18
NETOBSERV-2977: Add TLS support for collector when OpenShift #552
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
Open
leandroberetta
wants to merge
7
commits into
netobserv:main
Choose a base branch
from
leandroberetta:netobserv-2515
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+54,954
−5,676
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
bc15c36
grpc writer with tls in openshift
leandroberetta 1d3fbf0
feedback addressed
leandroberetta 1c48dea
update flp dependency
leandroberetta 8956222
NETOBSERV-2515: honor cluster TLS security profile on collector↔FLP gRPC
leandroberetta fb72f93
Bump go directive to 1.26 to match the toolchain and operator
leandroberetta c0ba4f1
Silence shellcheck SC2034 for the cross-file agentManifest global
leandroberetta 90e2ebe
e2e: update YAML snapshot tests for TLS resolver RBAC
leandroberetta File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,87 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "crypto/tls" | ||
| "os" | ||
|
|
||
| "google.golang.org/grpc" | ||
| "google.golang.org/grpc/credentials" | ||
|
|
||
| grpcCollector "github.com/netobserv/flowlogs-pipeline/pkg/pipeline/write/grpc" | ||
| "github.com/netobserv/flowlogs-pipeline/pkg/tlsprofile" | ||
| ) | ||
|
|
||
| const ( | ||
| collectorCertPath = "/etc/collector-tls/tls.crt" | ||
| collectorKeyPath = "/etc/collector-tls/tls.key" | ||
| ) | ||
|
|
||
| func collectorTLSOptions() []grpcCollector.CollectorOption { | ||
| return buildCollectorTLSOptions(collectorCertPath, collectorKeyPath) | ||
| } | ||
|
|
||
| // buildCollectorTLSOptions loads the given cert/key pair and returns the gRPC server options enabling | ||
| // TLS on the collector. It returns nil (TLS disabled) when either file is missing or the pair fails to | ||
| // load. The paths are parameters so this can be unit-tested against a temporary key pair. | ||
| func buildCollectorTLSOptions(certPath, keyPath string) []grpcCollector.CollectorOption { | ||
| tlsConfig := buildCollectorTLSConfig(certPath, keyPath) | ||
| if tlsConfig == nil { | ||
| return nil | ||
| } | ||
|
|
||
| log.Info("TLS enabled for collector") | ||
| return []grpcCollector.CollectorOption{ | ||
| grpcCollector.WithGRPCServerOptions(grpc.Creds(credentials.NewTLS(tlsConfig))), | ||
| } | ||
| } | ||
|
|
||
| // buildCollectorTLSConfig loads the cert/key pair and builds the server tls.Config, honoring the | ||
| // cluster TLS security profile through tlsprofile.Apply (TLS_MIN_VERSION / TLS_CIPHER_SUITES / | ||
| // TLS_CURVE_PREFERENCES, populated by the resolve-tls initContainer). No TLS version is hardcoded: | ||
| // the effective profile is always what the cluster dictates. Returns nil (TLS disabled) when either | ||
| // file is missing or the pair fails to load. | ||
| func buildCollectorTLSConfig(certPath, keyPath string) *tls.Config { | ||
| if _, err := os.Stat(certPath); err != nil { | ||
| return nil | ||
| } | ||
| if _, err := os.Stat(keyPath); err != nil { | ||
| return nil | ||
| } | ||
|
|
||
| cert, err := tls.LoadX509KeyPair(certPath, keyPath) | ||
| if err != nil { | ||
| log.Errorf("Failed to load TLS certificates: %v", err) | ||
| return nil | ||
| } | ||
|
|
||
| tlsConfig := &tls.Config{ | ||
| Certificates: []tls.Certificate{cert}, | ||
| } | ||
|
|
||
| applied, err := tlsprofile.Apply(tlsConfig) | ||
| if err != nil { | ||
| log.Errorf("Failed to apply TLS security profile: %v", err) | ||
| } else if !applied { | ||
| log.Warn("No TLS security profile settings found; relying on Go defaults") | ||
| } | ||
|
|
||
| return tlsConfig | ||
| } | ||
|
|
||
| func mockClientTLSConfig() *tls.Config { | ||
| return buildMockClientTLSConfig(collectorCertPath) | ||
| } | ||
|
|
||
| // buildMockClientTLSConfig returns a client TLS config (skipping verification, TLS 1.3) when the given | ||
| // cert file exists, or nil otherwise. The path is a parameter so this can be unit-tested. | ||
| // This is used only by the mock client (--mock); the collector server always supports TLS 1.3, so it | ||
| // stays pinned to 1.3 regardless of the cluster profile applied to the real server. | ||
| func buildMockClientTLSConfig(certPath string) *tls.Config { | ||
| if _, err := os.Stat(certPath); err != nil { | ||
| return nil | ||
| } | ||
| return &tls.Config{ | ||
| InsecureSkipVerify: true, | ||
| MinVersion: tls.VersionTLS13, | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,150 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "crypto/ecdsa" | ||
| "crypto/elliptic" | ||
| "crypto/rand" | ||
| "crypto/tls" | ||
| "crypto/x509" | ||
| "crypto/x509/pkix" | ||
| "encoding/pem" | ||
| "math/big" | ||
| "os" | ||
| "path/filepath" | ||
| "testing" | ||
| "time" | ||
|
|
||
| "github.com/stretchr/testify/assert" | ||
| ) | ||
|
|
||
| // writeSelfSignedCert generates a throwaway self-signed cert/key pair, writes it into dir and returns | ||
| // the cert and key paths. | ||
| func writeSelfSignedCert(t *testing.T, dir string) (string, string) { | ||
| t.Helper() | ||
|
|
||
| priv, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader) | ||
| if err != nil { | ||
| t.Fatalf("failed to generate key: %v", err) | ||
| } | ||
|
|
||
| template := x509.Certificate{ | ||
| SerialNumber: big.NewInt(1), | ||
| Subject: pkix.Name{CommonName: "collector-test"}, | ||
| NotBefore: time.Now().Add(-time.Hour), | ||
| NotAfter: time.Now().Add(time.Hour), | ||
| } | ||
| der, err := x509.CreateCertificate(rand.Reader, &template, &template, &priv.PublicKey, priv) | ||
| if err != nil { | ||
| t.Fatalf("failed to create certificate: %v", err) | ||
| } | ||
|
|
||
| keyDER, err := x509.MarshalECPrivateKey(priv) | ||
| if err != nil { | ||
| t.Fatalf("failed to marshal key: %v", err) | ||
| } | ||
|
|
||
| certPath := filepath.Join(dir, "tls.crt") | ||
| keyPath := filepath.Join(dir, "tls.key") | ||
| writeFile(t, certPath, pem.EncodeToMemory(&pem.Block{Type: "CERTIFICATE", Bytes: der})) | ||
| writeFile(t, keyPath, pem.EncodeToMemory(&pem.Block{Type: "EC PRIVATE KEY", Bytes: keyDER})) | ||
| return certPath, keyPath | ||
| } | ||
|
|
||
| func writeFile(t *testing.T, path string, data []byte) { | ||
| t.Helper() | ||
| if err := os.WriteFile(path, data, 0o600); err != nil { | ||
| t.Fatalf("failed to write %s: %v", path, err) | ||
| } | ||
| } | ||
|
|
||
| func TestBuildCollectorTLSOptions(t *testing.T) { | ||
| t.Run("returns nil when the cert file is missing", func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| assert.Nil(t, buildCollectorTLSOptions(filepath.Join(dir, "missing.crt"), filepath.Join(dir, "missing.key"))) | ||
| }) | ||
|
|
||
| t.Run("returns nil when the key file is missing", func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| certPath, _ := writeSelfSignedCert(t, dir) | ||
| assert.Nil(t, buildCollectorTLSOptions(certPath, filepath.Join(dir, "missing.key"))) | ||
| }) | ||
|
|
||
| t.Run("returns nil when the key pair is invalid", func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| certPath := filepath.Join(dir, "tls.crt") | ||
| keyPath := filepath.Join(dir, "tls.key") | ||
| writeFile(t, certPath, []byte("not a cert")) | ||
| writeFile(t, keyPath, []byte("not a key")) | ||
| assert.Nil(t, buildCollectorTLSOptions(certPath, keyPath)) | ||
| }) | ||
|
|
||
| t.Run("returns one server option for a valid key pair", func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| certPath, keyPath := writeSelfSignedCert(t, dir) | ||
| opts := buildCollectorTLSOptions(certPath, keyPath) | ||
| assert.Len(t, opts, 1) | ||
| }) | ||
| } | ||
|
|
||
| func TestBuildCollectorTLSConfig(t *testing.T) { | ||
| t.Run("returns nil when the cert file is missing", func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| assert.Nil(t, buildCollectorTLSConfig(filepath.Join(dir, "missing.crt"), filepath.Join(dir, "missing.key"))) | ||
| }) | ||
|
|
||
| t.Run("does not hardcode a min version when no profile env is set", func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| certPath, keyPath := writeSelfSignedCert(t, dir) | ||
| cfg := buildCollectorTLSConfig(certPath, keyPath) | ||
| if cfg == nil { | ||
| t.Fatal("expected a non-nil TLS config") | ||
| } | ||
| // No TLS_* env: nothing applied, so MinVersion stays 0 (Go decides), never a hardcoded 1.3. | ||
| assert.Equal(t, uint16(0), cfg.MinVersion) | ||
| assert.Len(t, cfg.Certificates, 1) | ||
| }) | ||
|
|
||
| t.Run("applies min version and cipher suites from the profile env (TLS 1.2)", func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| certPath, keyPath := writeSelfSignedCert(t, dir) | ||
| t.Setenv("TLS_MIN_VERSION", "771") // TLS 1.2 | ||
| t.Setenv("TLS_CIPHER_SUITES", "49199,49195") // ECDHE-RSA/ECDSA AES128-GCM | ||
| cfg := buildCollectorTLSConfig(certPath, keyPath) | ||
| if cfg == nil { | ||
| t.Fatal("expected a non-nil TLS config") | ||
| } | ||
| assert.Equal(t, uint16(tls.VersionTLS12), cfg.MinVersion) | ||
| assert.Equal(t, []uint16{49199, 49195}, cfg.CipherSuites) | ||
| }) | ||
|
|
||
| t.Run("ignores cipher suites when the profile pins TLS 1.3", func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| certPath, keyPath := writeSelfSignedCert(t, dir) | ||
| t.Setenv("TLS_MIN_VERSION", "772") // TLS 1.3 | ||
| t.Setenv("TLS_CIPHER_SUITES", "49199,49195") | ||
| cfg := buildCollectorTLSConfig(certPath, keyPath) | ||
| if cfg == nil { | ||
| t.Fatal("expected a non-nil TLS config") | ||
| } | ||
| assert.Equal(t, uint16(tls.VersionTLS13), cfg.MinVersion) | ||
| assert.Empty(t, cfg.CipherSuites) | ||
| }) | ||
| } | ||
|
|
||
| func TestBuildMockClientTLSConfig(t *testing.T) { | ||
| t.Run("returns nil when the cert file is missing", func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| assert.Nil(t, buildMockClientTLSConfig(filepath.Join(dir, "missing.crt"))) | ||
| }) | ||
|
|
||
| t.Run("returns an insecure TLS 1.3 config when the cert file exists", func(t *testing.T) { | ||
| dir := t.TempDir() | ||
| certPath, _ := writeSelfSignedCert(t, dir) | ||
| cfg := buildMockClientTLSConfig(certPath) | ||
| if cfg == nil { | ||
| t.Fatal("expected a non-nil TLS config") | ||
| } | ||
| assert.True(t, cfg.InsecureSkipVerify) | ||
| assert.Equal(t, uint16(tls.VersionTLS13), cfg.MinVersion) | ||
| }) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| package cmd | ||
|
|
||
| import ( | ||
| "context" | ||
|
|
||
| "github.com/netobserv/network-observability-cli/internal/pkg/tlsresolver" | ||
| "github.com/spf13/cobra" | ||
| ) | ||
|
|
||
| // resolveTLSCmd reads the cluster's OpenShift APIServer tlsSecurityProfile and writes the resolved | ||
| // numeric TLS settings to the collector-tls-config ConfigMap. It runs as the collector pod's | ||
| // initContainer so the ConfigMap exists before the collector server and the agents start. | ||
| var resolveTLSCmd = &cobra.Command{ | ||
| Use: "resolve-tls", | ||
| Short: "Resolve the cluster TLS security profile into a ConfigMap", | ||
| Long: `Reads the OpenShift APIServer tlsSecurityProfile and writes the corresponding | ||
| TLS_MIN_VERSION / TLS_CIPHER_SUITES / TLS_CURVE_PREFERENCES settings to the | ||
| collector-tls-config ConfigMap, so the collector and agents honor the cluster profile.`, | ||
| Run: runResolveTLS, | ||
| } | ||
|
|
||
| func runResolveTLS(_ *cobra.Command, _ []string) { | ||
| if err := tlsresolver.Resolve(context.Background(), namespace); err != nil { | ||
| log.Fatalf("failed to resolve TLS profile: %v", err) | ||
| } | ||
| log.Infof("TLS profile resolved into ConfigMap %s in namespace %s", tlsresolver.ConfigMapName, namespace) | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
that deserve at least a unit test