Skip to content

NETOBSERV-2977: Add TLS support for collector when OpenShift - #552

Open
leandroberetta wants to merge 9 commits into
netobserv:mainfrom
leandroberetta:netobserv-2515
Open

leandroberetta wants to merge 9 commits into
netobserv:mainfrom
leandroberetta:netobserv-2515

Conversation

@leandroberetta

@leandroberetta leandroberetta commented Aug 11, 2026

Copy link
Copy Markdown
Member

Description

Enable TLS for the collector↔agent gRPC connection when running on OpenShift, and make that connection honor the cluster's TLS security profile.

TLS enablement (service-ca)

  • Enable TLS for the gRPC collector server on OpenShift, using service-ca for automatic cert generation
  • Conditionally detect OpenShift and annotate the collector Service for cert generation, create a CA ConfigMap with inject-cabundle, and mount certs in the collector pod
  • Add CA volume and tls.caCertPath to the agent DaemonSet FLP config so agents verify the collector's certificate
  • On non-OpenShift clusters, everything works without TLS as before

Honor the cluster TLS security profile

  • Both ends previously hardcoded MinVersion: TLS 1.3. The connection now derives min version / cipher suites / curves from the OpenShift tlsSecurityProfile (apiservers.config.openshift.io/cluster), like every other netobserv component — no hardcoded version
  • A new resolve-tls subcommand runs as an initContainer on the collector pod: it reads the profile, resolves it, and writes the collector-tls-config ConfigMap. Both the collector container and the agent DaemonSet consume it via envFrom, so a single resolver run drives both ends
  • When the cluster has no explicit profile, it falls back to the Intermediate preset, mirroring the operator's default (no invented default)
  • Install order flips on the TLS path (collector first, then agents) so the ConfigMap exists before agents start; agents reference it with optional: false to fail loudly rather than silently downgrade
  • OpenShift-only, flows/packets only; metrics, --yaml, and non-OCP runs are unchanged

How to test

Scope

This change only affects oc netobserv flows and oc netobserv packets running against an OpenShift cluster. Everything else is expected to behave exactly as on main:

  • oc netobserv metrics — unchanged (no collector pod involved)
  • --yaml output — unchanged (no TLS resources are added to the generated manifests)
  • Non-OpenShift clusters (kind, vanilla k8s) — unchanged, capture runs in plaintext as before

Prerequisites

  • An OpenShift cluster with the service-ca operator running (standard on OCP)

  • cluster-admin, since the capture creates a namespace, SCC and a ClusterRole

  • The CLI build from this PR. CI posts a comment with the image and the exact make commands line to run once the PR carries the ok-to-test label. If you build locally instead:

    USER=<your-quay-user> VERSION=<tag> make images push commands
    export NETOBSERV_COLLECTOR_IMAGE=quay.io/<your-quay-user>/network-observability-cli:<tag>

All captures below run in the netobserv-cli namespace by default (override with NETOBSERV_NAMESPACE).

Terminology used below

The resolver writes the cluster's TLS profile into a ConfigMap as decimal values. For TLS_MIN_VERSION:

Value TLS version
769 TLS 1.0
770 TLS 1.1
771 TLS 1.2
772 TLS 1.3

Scenario 1 — Default cluster (no explicit tlsSecurityProfile)

A stock cluster has no spec.tlsSecurityProfile set on the APIServer, and the CLI must fall back to the Intermediate preset (the same default the operator uses).

  1. Confirm the cluster has no explicit profile:

    oc get apiserver cluster -o jsonpath='{.spec.tlsSecurityProfile}'   # expect empty
  2. Start a capture and leave it running:

    oc netobserv flows --max-time=5m
  3. Expected CLI output — these lines must appear, in this order:

    OpenShift detected, enabling TLS for collector
    creating collector service
    creating CA configmap for TLS
    ...
    creating capture agents
    

    Note the ordering: on the TLS path the collector is created first, and the agents only after the collector pod is Ready. This is intentional — the agents mount a ConfigMap the collector's initContainer produces.

  4. From a second terminal, check the resources:

    # service-ca generated the server cert from the Service annotation
    oc -n netobserv-cli get svc collector -o jsonpath='{.metadata.annotations}' | grep serving-cert-secret-name
    oc -n netobserv-cli get secret collector-tls
    
    # service-ca injected the CA bundle
    oc -n netobserv-cli get cm collector-ca -o jsonpath='{.data.service-ca\.crt}' | head -1
    
    # the resolver initContainer ran and wrote the resolved profile
    oc -n netobserv-cli get pod collector -o jsonpath='{.spec.initContainers[*].name}'   # expect: resolve-tls
    oc -n netobserv-cli logs collector -c resolve-tls
    oc -n netobserv-cli get cm collector-tls-config -o yaml

    Expected: collector-tls-config exists and contains TLS_MIN_VERSION: "771" (TLS 1.2 = Intermediate), plus non-empty TLS_CIPHER_SUITES and TLS_CURVE_PREFERENCES. The resolve-tls log should say it resolved profile Intermediate.

  5. Check both ends picked it up:

    # collector server
    oc -n netobserv-cli logs collector -c collector | grep -i "TLS enabled for collector"
    oc -n netobserv-cli get pod collector -o jsonpath='{.spec.containers[?(@.name=="collector")].envFrom}'
    
    # agents: CA mounted + same ConfigMap consumed + FLP configured to verify the cert
    oc -n netobserv-cli get ds netobserv-cli -o yaml | grep -A3 collector-ca
    oc -n netobserv-cli get ds netobserv-cli -o jsonpath='{.spec.template.spec.containers[0].envFrom}'
    oc -n netobserv-cli get ds netobserv-cli -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="FLP_CONFIG")].value}' | grep caCertPath

    Expected: caCertPath: /etc/collector-ca/service-ca.crt in the FLP config, and configMapRef: collector-tls-config with optional: false on both the collector and the DaemonSet.

  6. Most important check — flows still arrive. The capture table must populate with flows and the agent logs must be free of TLS handshake errors:

    oc -n netobserv-cli logs ds/netobserv-cli | grep -i "tls\|x509\|handshake\|certificate"

    Expected: no errors. A working capture here is the real proof the whole chain (cert generation → CA injection → profile resolution → mutual agreement on cipher/version) lines up.

  7. Let the capture finish (or Ctrl-C), answer the copy prompt, and confirm the output files are written as usual.


Scenario 2 — Packets capture

Repeat scenario 1 with:

oc netobserv packets --max-time=5m --port=6443

Expected: identical TLS behavior (this path previously did not even create the CA ConfigMap), and packets captured normally.


Scenario 3 — Explicit Modern profile

This is the check that the connection genuinely follows the cluster profile rather than a hardcoded value.

oc patch apiserver cluster --type=merge -p '{"spec":{"tlsSecurityProfile":{"type":"Modern","modern":{}}}}'
# wait for the rollout to complete
oc get co

Then run a capture and inspect the ConfigMap:

oc netobserv flows --max-time=5m
oc -n netobserv-cli get cm collector-tls-config -o yaml

Expected: TLS_MIN_VERSION: "772" (TLS 1.3), and the capture works end to end.

Optional on-the-wire confirmation — a TLS 1.2 client must now be rejected:

oc -n netobserv-cli run tlscheck --rm -i --restart=Never \
  --image=registry.access.redhat.com/ubi9/ubi -- \
  openssl s_client -connect collector.netobserv-cli.svc:9999 -tls1_2 </dev/null

Expected with Modern: handshake failure (protocol version alert).
Expected with Intermediate: handshake succeeds and reports Protocol : TLSv1.2.


Scenario 4 — Explicit Old profile

oc patch apiserver cluster --type=merge -p '{"spec":{"tlsSecurityProfile":{"type":"Old","old":{}}}}'
# wait for the rollout

Expected: TLS_MIN_VERSION: "769" (TLS 1.0) in collector-tls-config, and the capture still works. The point here is that the CLI does not refuse or silently upgrade a permissive cluster profile.


Scenario 5 — Custom profile

oc patch apiserver cluster --type=merge -p '{
  "spec": {"tlsSecurityProfile": {
    "type": "Custom",
    "custom": {
      "ciphers": ["ECDHE-ECDSA-AES256-GCM-SHA384","ECDHE-RSA-AES256-GCM-SHA384"],
      "minTLSVersion": "VersionTLS12"
    }}}}'
# wait for the rollout

Expected: TLS_MIN_VERSION: "771" and a TLS_CIPHER_SUITES list restricted to the two suites requested (as decimal IDs). Capture works.

Remember to restore the cluster afterwards:

oc patch apiserver cluster --type=json -p '[{"op":"remove","path":"/spec/tlsSecurityProfile"}]'

Scenario 6 — Regression: paths that must be untouched

Case Command Expected
Metrics oc netobserv metrics --max-time=5m No resolve-tls initContainer, no collector-tls* resources, metrics dashboard works as on main
YAML output oc netobserv flows --yaml Generated capture.yml contains no TLS volumes, initContainer or ConfigMap refs; applying it still works
Non-OpenShift run flows against a kind / vanilla k8s cluster Message Can't check version since cluster is not OpenShift, plaintext capture, no TLS resources created, flows arrive
Background oc netobserv flows --background then oc netobserv follow / stop / copy Works as on main, with TLS transparently enabled
Cleanup after any capture oc get ns netobserv-cli returns NotFound — the namespace deletion takes the Role, RoleBinding, Secret and both ConfigMaps with it

Scenario 7 — Fail-loud behavior (negative test)

The agents reference collector-tls-config with optional: false on purpose: a missing ConfigMap must make the agent pods fail visibly rather than silently fall back to plaintext.

To simulate, in one terminal start a capture, and as soon as the agents come up delete the ConfigMap and restart the DaemonSet:

oc -n netobserv-cli delete cm collector-tls-config
oc -n netobserv-cli rollout restart ds/netobserv-cli
oc -n netobserv-cli get pods -w

Expected: agent pods stuck in CreateContainerConfigError with an event naming the missing ConfigMap. They must not start and send flows in plaintext.


New RBAC to sanity-check

The capture now grants itself two extra permissions. Confirm they are present and no broader than described:

# cluster-scoped: read-only, restricted to the single 'cluster' APIServer object
oc get clusterrole netobserv-cli -o yaml | grep -A8 config.openshift.io

# namespaced: write the resolved ConfigMap in the run namespace only
oc -n netobserv-cli get role netobserv-cli -o yaml
oc -n netobserv-cli get rolebinding netobserv-cli -o yaml

Expected: get on apiservers limited via resourceNames: [cluster], and get/create/update on configmaps scoped to the run namespace (no delete, no cluster-wide ConfigMap access).

Dependencies

n/a

Checklist

  • Does the changes in PR need specific configuration or environment set up for testing?
    • if so please describe it in PR description.
  • I have added thorough unit tests for the change.
  • QE requirements (check 1 from the list):
    • Standard QE validation, with pre-merge tests unless stated otherwise.
    • Regression tests only (e.g. refactoring with no user-facing change).
    • No QE (e.g. trivial change with high reviewer's confidence, or per agreement with the QE team).

@openshift-ci-robot

openshift-ci-robot commented Aug 11, 2026

Copy link
Copy Markdown
Collaborator

@leandroberetta: This pull request references NETOBSERV-2515 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the epic to target either version "5.0.0." or "openshift-5.0.0.", but it targets "netobserv-2.0" instead.

Details

In response to this:

Description

  • Enable TLS for the gRPC collector server when running on OpenShift, using service-ca for automatic cert generation
  • Conditionally detect OpenShift and annotate the collector Service for cert generation, create a CA ConfigMap with inject-cabundle, and mount certs in the collector pod
  • Add CA volume and tls.caCertPath to the agent DaemonSet FLP config so agents verify the collector's certificate
  • On non-OpenShift clusters, everything works without TLS as before

Test plan

  • Verified on OpenShift 4.22: collector starts with TLS, agents connect and flows are received
  • Verify on non-OpenShift (vanilla k8s): TLS is skipped, flows work without TLS
  • Verify packet capture works with TLS
  • Verify background mode works with TLS

Dependencies

n/a

Checklist

  • Does the changes in PR need specific configuration or environment set up for testing?
    • if so please describe it in PR description.
  • I have added thorough unit tests for the change.
  • QE requirements (check 1 from the list):
  • Standard QE validation, with pre-merge tests unless stated otherwise.
  • Regression tests only (e.g. refactoring with no user-facing change).
  • No QE (e.g. trivial change with high reviewer's confidence, or per agreement with the QE team).

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

Comment thread go.mod Outdated
sigs.k8s.io/yaml v1.6.0 // indirect
)

replace github.com/netobserv/flowlogs-pipeline => github.com/leandroberetta/flowlogs-pipeline v0.0.0-20260810170916-6c5c94ab0294

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.

Don't forget to remove this :)

Comment thread cmd/collector_tls.go

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.

that deserve at least a unit test

Comment thread commands/netobserv Outdated
Comment on lines +213 to +224
if [[ "$tlsEnabled" == "true" ]]; then
cmd="${K8S_CLI_BIN} run -n $namespace collector \\
--image=$img --image-pull-policy='Always' --restart='Never' \\
--override-type=strategic \\
--overrides=$overrides \\
--command -- $runCommand"
else
cmd="${K8S_CLI_BIN} run -n $namespace collector \\
--image=$img --image-pull-policy='Always' --restart='Never' \\
--overrides=$overrides \\
--command -- $runCommand"
fi

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.

Suggested change
if [[ "$tlsEnabled" == "true" ]]; then
cmd="${K8S_CLI_BIN} run -n $namespace collector \\
--image=$img --image-pull-policy='Always' --restart='Never' \\
--override-type=strategic \\
--overrides=$overrides \\
--command -- $runCommand"
else
cmd="${K8S_CLI_BIN} run -n $namespace collector \\
--image=$img --image-pull-policy='Always' --restart='Never' \\
--overrides=$overrides \\
--command -- $runCommand"
fi
overrideType=""
if [[ "$tlsEnabled" == "true" ]]; then
overrideType="--override-type=strategic"
fi
cmd="${K8S_CLI_BIN} run -n $namespace collector \
--image=$img --image-pull-policy='Always' --restart='Never' \
$overrideType --overrides=$overrides \
--command -- $runCommand"

Comment thread scripts/functions.sh Outdated
Comment on lines 420 to 439

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.

We should simplify this to something like:

# Create collector service for flows/packets captures
if [[ "$command" = "flows" || "$command" = "packets" ]]; then
  echo "creating collector service"
  applyYAML "$collectorServiceYAML"
  if [[ "$tlsEnabled" == "true" ]]; then
    echo "creating CA configmap for TLS"
    createCAConfigMap
  fi
fi
if [ "$command" = "flows" ]; then
  echo "creating flow-capture agents"
elif [ "$command" = "packets" ]; then
  echo "creating packet-capture agents"
elif [ "$command" = "metrics" ]; then
  echo "creating service monitor"
  applyYAML "$smYAML"
  echo "creating metric-capture agents:"

Comment thread scripts/functions.sh Outdated
Comment on lines +158 to +160
function isOpenShift() {
${K8S_CLI_BIN} get clusterversion version &>/dev/null
}

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.

You should rely on checkClusterVersion here instead.

Feel free to add a global variable like isOCP in it for your usage 😉

Comment thread go.mod Outdated
golang.org/x/tools v0.45.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.81.1 // indirect
google.golang.org/grpc v1.82.0

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.

nit: Is that needed here ?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

It's a direct dependency now: collector_tls.go uses grpc.Creds(credentials.NewTLS(...)) to enable TLS on the collector, so grpc is imported directly rather than transitively.

@leandroberetta

Copy link
Copy Markdown
Member Author

@jpinsonneau I addressed the feedback, the only missing one is the dependency update, I'm waiting to merge this: netobserv/flowlogs-pipeline#1297

@openshift-ci-robot

openshift-ci-robot commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

@leandroberetta: This pull request references NETOBSERV-2515 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the epic to target either version "5.1.0." or "openshift-5.1.0.", but it targets "netobserv-2.0" instead.

Details

In response to this:

Description

Enable TLS for the collector↔agent gRPC connection when running on OpenShift, and make that connection honor the cluster's TLS security profile.

TLS enablement (service-ca)

  • Enable TLS for the gRPC collector server on OpenShift, using service-ca for automatic cert generation
  • Conditionally detect OpenShift and annotate the collector Service for cert generation, create a CA ConfigMap with inject-cabundle, and mount certs in the collector pod
  • Add CA volume and tls.caCertPath to the agent DaemonSet FLP config so agents verify the collector's certificate
  • On non-OpenShift clusters, everything works without TLS as before

Honor the cluster TLS security profile

  • Both ends previously hardcoded MinVersion: TLS 1.3. The connection now derives min version / cipher suites / curves from the OpenShift tlsSecurityProfile (apiservers.config.openshift.io/cluster), like every other netobserv component — no hardcoded version
  • A new resolve-tls subcommand runs as an initContainer on the collector pod: it reads the profile, resolves it, and writes the collector-tls-config ConfigMap. Both the collector container and the agent DaemonSet consume it via envFrom, so a single resolver run drives both ends
  • When the cluster has no explicit profile, it falls back to the Intermediate preset, mirroring the operator's default (no invented default)
  • Install order flips on the TLS path (collector first, then agents) so the ConfigMap exists before agents start; agents reference it with optional: false to fail loudly rather than silently downgrade
  • OpenShift-only, flows/packets only; metrics, --yaml, and non-OCP runs are unchanged

Dependencies

n/a

Checklist

  • Does the changes in PR need specific configuration or environment set up for testing?
    • if so please describe it in PR description.
  • I have added thorough unit tests for the change.
  • QE requirements (check 1 from the list):
  • Standard QE validation, with pre-merge tests unless stated otherwise.
  • Regression tests only (e.g. refactoring with no user-facing change).
  • No QE (e.g. trivial change with high reviewer's confidence, or per agreement with the QE team).

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@leandroberetta leandroberetta changed the title NETOBSERV-2515: Add TLS support for collector when OpenShift NETOBSERV-2977: Add TLS support for collector when OpenShift Sep 2, 2026
@openshift-ci-robot

openshift-ci-robot commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

@leandroberetta: This pull request references NETOBSERV-2977 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target the "5.1.0" version, but no target version was set.

Details

In response to this:

Description

Enable TLS for the collector↔agent gRPC connection when running on OpenShift, and make that connection honor the cluster's TLS security profile.

TLS enablement (service-ca)

  • Enable TLS for the gRPC collector server on OpenShift, using service-ca for automatic cert generation
  • Conditionally detect OpenShift and annotate the collector Service for cert generation, create a CA ConfigMap with inject-cabundle, and mount certs in the collector pod
  • Add CA volume and tls.caCertPath to the agent DaemonSet FLP config so agents verify the collector's certificate
  • On non-OpenShift clusters, everything works without TLS as before

Honor the cluster TLS security profile

  • Both ends previously hardcoded MinVersion: TLS 1.3. The connection now derives min version / cipher suites / curves from the OpenShift tlsSecurityProfile (apiservers.config.openshift.io/cluster), like every other netobserv component — no hardcoded version
  • A new resolve-tls subcommand runs as an initContainer on the collector pod: it reads the profile, resolves it, and writes the collector-tls-config ConfigMap. Both the collector container and the agent DaemonSet consume it via envFrom, so a single resolver run drives both ends
  • When the cluster has no explicit profile, it falls back to the Intermediate preset, mirroring the operator's default (no invented default)
  • Install order flips on the TLS path (collector first, then agents) so the ConfigMap exists before agents start; agents reference it with optional: false to fail loudly rather than silently downgrade
  • OpenShift-only, flows/packets only; metrics, --yaml, and non-OCP runs are unchanged

Dependencies

n/a

Checklist

  • Does the changes in PR need specific configuration or environment set up for testing?
    • if so please describe it in PR description.
  • I have added thorough unit tests for the change.
  • QE requirements (check 1 from the list):
  • Standard QE validation, with pre-merge tests unless stated otherwise.
  • Regression tests only (e.g. refactoring with no user-facing change).
  • No QE (e.g. trivial change with high reviewer's confidence, or per agreement with the QE team).

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@codecov

codecov Bot commented Sep 7, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 61.88341% with 85 lines in your changes missing coverage. Please review.
✅ Project coverage is 17.31%. Comparing base (d924765) to head (a2b51bb).
⚠️ Report is 4 commits behind head on main.

Files with missing lines Patch % Lines
internal/pkg/tlsresolver/resolve.go 43.85% 26 Missing and 6 partials ⚠️
e2e/common.go 0.00% 24 Missing ⚠️
internal/pkg/tlsresolver/config.go 85.00% 14 Missing and 1 partial ⚠️
cmd/collector_tls.go 79.41% 5 Missing and 2 partials ⚠️
cmd/resolve_tls.go 0.00% 4 Missing ⚠️
cmd/flow_capture.go 0.00% 1 Missing ⚠️
cmd/mocks.go 0.00% 1 Missing ⚠️
cmd/packet_capture.go 0.00% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #552      +/-   ##
==========================================
+ Coverage   13.18%   17.31%   +4.13%     
==========================================
  Files          20       24       +4     
  Lines        2443     2656     +213     
==========================================
+ Hits          322      460     +138     
- Misses       2095     2161      +66     
- Partials       26       35       +9     
Flag Coverage Δ
unittests 17.31% <61.88%> (+4.13%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
cmd/root.go 28.42% <100.00%> (+0.76%) ⬆️
cmd/flow_capture.go 7.59% <0.00%> (ø)
cmd/mocks.go 0.00% <0.00%> (ø)
cmd/packet_capture.go 0.00% <0.00%> (ø)
cmd/resolve_tls.go 0.00% <0.00%> (ø)
cmd/collector_tls.go 79.41% <79.41%> (ø)
internal/pkg/tlsresolver/config.go 85.00% <85.00%> (ø)
e2e/common.go 0.00% <0.00%> (ø)
internal/pkg/tlsresolver/resolve.go 43.85% <43.85%> (ø)
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@leandroberetta leandroberetta self-assigned this Sep 8, 2026
@leandroberetta leandroberetta added the needs-review Tells that the PR needs a review label Sep 8, 2026
jpinsonneau
jpinsonneau previously approved these changes Sep 11, 2026

@jpinsonneau jpinsonneau left a comment

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.

LGTM ! Thanks @leandroberetta

@kapjain-rh

Copy link
Copy Markdown
Member

/ok-to-test

leandroberetta and others added 9 commits September 15, 2026 09:41
The gRPC connection between the capture agent (FLP client) and the CLI
collector (gRPC server) hardcoded MinVersion: TLS 1.3 on both ends. Every
other netobserv component derives its TLS settings from the OpenShift
tlsSecurityProfile (apiservers.config.openshift.io/cluster); the CLI now
does the same instead of pinning a version.

A new `resolve-tls` subcommand runs as an initContainer on the collector
pod. It reads the cluster's tlsSecurityProfile, resolves it to concrete
min version / cipher suites / curves (falling back to the Intermediate
preset when no profile is set, mirroring the operator's default), and
writes them into the `collector-tls-config` ConfigMap. Both the collector
container and the agent DaemonSet consume that ConfigMap via envFrom, so a
single resolver run drives both ends. The collector server applies the
resolved settings through flowlogs-pipeline's tlsprofile.Apply.

Because the ConfigMap only exists once the collector's init completes, the
install order flips on the TLS path: the collector is created and waited on
first, then the agents (which reference the ConfigMap with optional:false,
failing loudly rather than silently downgrading TLS).

This path stays OpenShift-only and applies to flows/packets captures;
metrics, --yaml output and non-OCP runs are unchanged.

- internal/pkg/tlsresolver: profile resolution + ConfigMap write, with tests
- cmd/resolve_tls.go, cmd/root.go: new resolve-tls subcommand
- cmd/collector_tls.go: drop hardcoded TLS 1.3, apply resolved profile
- res/service-account.yml: RBAC to read apiservers/cluster and write the CM
- commands/netobserv, scripts/functions.sh: initContainer, envFrom, ordering
- go.mod: add openshift/api and openshift/library-go/pkg/crypto

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The TLS profile resolver maps the OpenShift SecP256r1MLKEM768 /
SecP384r1MLKEM1024 groups to their crypto/tls.CurveID constants, which are
Go 1.26+. CI (setup-go 1.26), the Dockerfile builder (golang:1.26) and the
operator (go 1.26.3) are already on 1.26; only the go.mod directive lagged
at 1.25.7, which made govet's stdversion reject those constants and the
exhaustive linter reject dropping them. Bump the directive to 1.26.0.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
agentManifest is set in functions.sh setup() but consumed in
commands/netobserv (applied after the collector is ready). shellcheck
analyzes each file in isolation, so it flags the assignment as unused;
add the same disable directive the file already uses elsewhere.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Moving the resolve-tls initContainer's namespaced Role and RoleBinding to
the end of res/service-account.yml keeps the existing service-account
document indices stable, but still adds two documents to every generated
capture manifest. Update the positional assertions in the flow, packet and
metric YAML e2e tests accordingly:

- bump the expected document counts (8 -> 10 for flows/packets,
  12 -> 14 for metrics),
- assert the new Role (configmaps get/create/update) at index [6] and
  RoleBinding (ServiceAccount -> Role netobserv-cli) at index [7],
- assert the new config.openshift.io/apiservers get rule on the
  netobserv-cli ClusterRole,
- shift the collector Service / DaemonSet and the metric-specific
  documents to their new indices.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
go.mod requires go >= 1.26.0 (the TLS profile resolver uses the
crypto/tls SecP256r1MLKEM768 / SecP384r1MLKEM1024 CurveID constants,
added in Go 1.26), but the build root was still
rhel-9-release-golang-1.25-openshift-4.21, which ships Go 1.25.12 with
GOTOOLCHAIN=local. The netobserv-cli-tests step therefore failed
immediately on `go list -m github.com/onsi/ginkgo/v2` with
"go.mod requires go >= 1.26.0 (running go 1.25.12; GOTOOLCHAIN=local)".

Move to rhel-9-release-golang-1.26-openshift-5.0, the build root already
used by network-observability-operator, flowlogs-pipeline and
netobserv-ebpf-agent.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
StartCommand called pty.Start inside a goroutine and only did `_ = ptmx`,
which does not extend the file's lifetime past that goroutine. The runtime
finalizer then closed the PTY master, the kernel sent SIGHUP to the
foreground process group, and the CLI died about 20s in -- running its EXIT
trap, which deletes the daemonset, the collector pod and the namespace.
`--max-time` was therefore never honoured in e2e; the whole suite really had
a ~20s budget, and StartCommandWait was bumped to 20s to snapshot the output
just before the process was killed.

That was survivable while the daemonset was created in setup(), a second
into the run. With the collector TLS path it is created only once the
collector is ready (the agents consume the collector-tls-config ConfigMap
written by the resolve-tls initContainer), i.e. about 8s in, leaving ~13s
before the SIGHUP -- not enough for the agent pods to report ready, so
"Verify all CLI pods are deployed" polled a daemonset that no longer existed
until it timed out:

  17:33:59.4  create daemonsets/netobserv-cli
  17:34:12.4  delete daemonsets/netobserv-cli   <- CLI EXIT trap after SIGHUP
  17:34:16    test starts seeing NotFound, for 10 minutes

Hold the PTY master in a package-level slice and drain it, so the command
runs until its own --max-time. Since captures now outlive the spec, add
StopStartedCommands and call it from cleanup(): otherwise a lingering CLI
would fire its EXIT trap while the next spec is setting up and delete that
spec's namespace.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@openshift-ci

openshift-ci Bot commented Sep 15, 2026

Copy link
Copy Markdown

New changes are detected. LGTM label has been removed.

@openshift-ci

openshift-ci Bot commented Sep 15, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please ask for approval from jpinsonneau. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@openshift-ci-robot

openshift-ci-robot commented Sep 15, 2026

Copy link
Copy Markdown
Collaborator

@leandroberetta: This pull request references NETOBSERV-2977 which is a valid jira issue.

Warning: The referenced jira issue has an invalid target version for the target branch this PR targets: expected the story to target either version "5.1.0." or "openshift-5.1.0.", but it targets "netobserv-2.0" instead.

Details

In response to this:

Description

Enable TLS for the collector↔agent gRPC connection when running on OpenShift, and make that connection honor the cluster's TLS security profile.

TLS enablement (service-ca)

  • Enable TLS for the gRPC collector server on OpenShift, using service-ca for automatic cert generation
  • Conditionally detect OpenShift and annotate the collector Service for cert generation, create a CA ConfigMap with inject-cabundle, and mount certs in the collector pod
  • Add CA volume and tls.caCertPath to the agent DaemonSet FLP config so agents verify the collector's certificate
  • On non-OpenShift clusters, everything works without TLS as before

Honor the cluster TLS security profile

  • Both ends previously hardcoded MinVersion: TLS 1.3. The connection now derives min version / cipher suites / curves from the OpenShift tlsSecurityProfile (apiservers.config.openshift.io/cluster), like every other netobserv component — no hardcoded version
  • A new resolve-tls subcommand runs as an initContainer on the collector pod: it reads the profile, resolves it, and writes the collector-tls-config ConfigMap. Both the collector container and the agent DaemonSet consume it via envFrom, so a single resolver run drives both ends
  • When the cluster has no explicit profile, it falls back to the Intermediate preset, mirroring the operator's default (no invented default)
  • Install order flips on the TLS path (collector first, then agents) so the ConfigMap exists before agents start; agents reference it with optional: false to fail loudly rather than silently downgrade
  • OpenShift-only, flows/packets only; metrics, --yaml, and non-OCP runs are unchanged

How to test

Scope

This change only affects oc netobserv flows and oc netobserv packets running against an OpenShift cluster. Everything else is expected to behave exactly as on main:

  • oc netobserv metrics — unchanged (no collector pod involved)
  • --yaml output — unchanged (no TLS resources are added to the generated manifests)
  • Non-OpenShift clusters (kind, vanilla k8s) — unchanged, capture runs in plaintext as before

Prerequisites

  • An OpenShift cluster with the service-ca operator running (standard on OCP)
  • cluster-admin, since the capture creates a namespace, SCC and a ClusterRole
  • The CLI build from this PR. CI posts a comment with the image and the exact make commands line to run once the PR carries the ok-to-test label. If you build locally instead:
USER=<your-quay-user> VERSION=<tag> make images push commands
export NETOBSERV_COLLECTOR_IMAGE=quay.io/<your-quay-user>/network-observability-cli:<tag>

All captures below run in the netobserv-cli namespace by default (override with NETOBSERV_NAMESPACE).

Terminology used below

The resolver writes the cluster's TLS profile into a ConfigMap as decimal values. For TLS_MIN_VERSION:

Value TLS version
769 TLS 1.0
770 TLS 1.1
771 TLS 1.2
772 TLS 1.3

Scenario 1 — Default cluster (no explicit tlsSecurityProfile)

A stock cluster has no spec.tlsSecurityProfile set on the APIServer, and the CLI must fall back to the Intermediate preset (the same default the operator uses).

  1. Confirm the cluster has no explicit profile:
oc get apiserver cluster -o jsonpath='{.spec.tlsSecurityProfile}'   # expect empty
  1. Start a capture and leave it running:
oc netobserv flows --max-time=5m
  1. Expected CLI output — these lines must appear, in this order:
OpenShift detected, enabling TLS for collector
creating collector service
creating CA configmap for TLS
...
creating capture agents

Note the ordering: on the TLS path the collector is created first, and the agents only after the collector pod is Ready. This is intentional — the agents mount a ConfigMap the collector's initContainer produces.

  1. From a second terminal, check the resources:
# service-ca generated the server cert from the Service annotation
oc -n netobserv-cli get svc collector -o jsonpath='{.metadata.annotations}' | grep serving-cert-secret-name
oc -n netobserv-cli get secret collector-tls

# service-ca injected the CA bundle
oc -n netobserv-cli get cm collector-ca -o jsonpath='{.data.service-ca\.crt}' | head -1

# the resolver initContainer ran and wrote the resolved profile
oc -n netobserv-cli get pod collector -o jsonpath='{.spec.initContainers[*].name}'   # expect: resolve-tls
oc -n netobserv-cli logs collector -c resolve-tls
oc -n netobserv-cli get cm collector-tls-config -o yaml

Expected: collector-tls-config exists and contains TLS_MIN_VERSION: "771" (TLS 1.2 = Intermediate), plus non-empty TLS_CIPHER_SUITES and TLS_CURVE_PREFERENCES. The resolve-tls log should say it resolved profile Intermediate.

  1. Check both ends picked it up:
# collector server
oc -n netobserv-cli logs collector -c collector | grep -i "TLS enabled for collector"
oc -n netobserv-cli get pod collector -o jsonpath='{.spec.containers[?(@.name=="collector")].envFrom}'

# agents: CA mounted + same ConfigMap consumed + FLP configured to verify the cert
oc -n netobserv-cli get ds netobserv-cli -o yaml | grep -A3 collector-ca
oc -n netobserv-cli get ds netobserv-cli -o jsonpath='{.spec.template.spec.containers[0].envFrom}'
oc -n netobserv-cli get ds netobserv-cli -o jsonpath='{.spec.template.spec.containers[0].env[?(@.name=="FLP_CONFIG")].value}' | grep caCertPath

Expected: caCertPath: /etc/collector-ca/service-ca.crt in the FLP config, and configMapRef: collector-tls-config with optional: false on both the collector and the DaemonSet.

  1. Most important check — flows still arrive. The capture table must populate with flows and the agent logs must be free of TLS handshake errors:
oc -n netobserv-cli logs ds/netobserv-cli | grep -i "tls\|x509\|handshake\|certificate"

Expected: no errors. A working capture here is the real proof the whole chain (cert generation → CA injection → profile resolution → mutual agreement on cipher/version) lines up.

  1. Let the capture finish (or Ctrl-C), answer the copy prompt, and confirm the output files are written as usual.

Scenario 2 — Packets capture

Repeat scenario 1 with:

oc netobserv packets --max-time=5m --port=6443

Expected: identical TLS behavior (this path previously did not even create the CA ConfigMap), and packets captured normally.


Scenario 3 — Explicit Modern profile

This is the check that the connection genuinely follows the cluster profile rather than a hardcoded value.

oc patch apiserver cluster --type=merge -p '{"spec":{"tlsSecurityProfile":{"type":"Modern","modern":{}}}}'
# wait for the rollout to complete
oc get co

Then run a capture and inspect the ConfigMap:

oc netobserv flows --max-time=5m
oc -n netobserv-cli get cm collector-tls-config -o yaml

Expected: TLS_MIN_VERSION: "772" (TLS 1.3), and the capture works end to end.

Optional on-the-wire confirmation — a TLS 1.2 client must now be rejected:

oc -n netobserv-cli run tlscheck --rm -i --restart=Never \
 --image=registry.access.redhat.com/ubi9/ubi -- \
 openssl s_client -connect collector.netobserv-cli.svc:9999 -tls1_2 </dev/null

Expected with Modern: handshake failure (protocol version alert).
Expected with Intermediate: handshake succeeds and reports Protocol : TLSv1.2.


Scenario 4 — Explicit Old profile

oc patch apiserver cluster --type=merge -p '{"spec":{"tlsSecurityProfile":{"type":"Old","old":{}}}}'
# wait for the rollout

Expected: TLS_MIN_VERSION: "769" (TLS 1.0) in collector-tls-config, and the capture still works. The point here is that the CLI does not refuse or silently upgrade a permissive cluster profile.


Scenario 5 — Custom profile

oc patch apiserver cluster --type=merge -p '{
 "spec": {"tlsSecurityProfile": {
   "type": "Custom",
   "custom": {
     "ciphers": ["ECDHE-ECDSA-AES256-GCM-SHA384","ECDHE-RSA-AES256-GCM-SHA384"],
     "minTLSVersion": "VersionTLS12"
   }}}}'
# wait for the rollout

Expected: TLS_MIN_VERSION: "771" and a TLS_CIPHER_SUITES list restricted to the two suites requested (as decimal IDs). Capture works.

Remember to restore the cluster afterwards:

oc patch apiserver cluster --type=json -p '[{"op":"remove","path":"/spec/tlsSecurityProfile"}]'

Scenario 6 — Regression: paths that must be untouched

Case Command Expected
Metrics oc netobserv metrics --max-time=5m No resolve-tls initContainer, no collector-tls* resources, metrics dashboard works as on main
YAML output oc netobserv flows --yaml Generated capture.yml contains no TLS volumes, initContainer or ConfigMap refs; applying it still works
Non-OpenShift run flows against a kind / vanilla k8s cluster Message Can't check version since cluster is not OpenShift, plaintext capture, no TLS resources created, flows arrive
Background oc netobserv flows --background then oc netobserv follow / stop / copy Works as on main, with TLS transparently enabled
Cleanup after any capture oc get ns netobserv-cli returns NotFound — the namespace deletion takes the Role, RoleBinding, Secret and both ConfigMaps with it

Scenario 7 — Fail-loud behavior (negative test)

The agents reference collector-tls-config with optional: false on purpose: a missing ConfigMap must make the agent pods fail visibly rather than silently fall back to plaintext.

To simulate, in one terminal start a capture, and as soon as the agents come up delete the ConfigMap and restart the DaemonSet:

oc -n netobserv-cli delete cm collector-tls-config
oc -n netobserv-cli rollout restart ds/netobserv-cli
oc -n netobserv-cli get pods -w

Expected: agent pods stuck in CreateContainerConfigError with an event naming the missing ConfigMap. They must not start and send flows in plaintext.


New RBAC to sanity-check

The capture now grants itself two extra permissions. Confirm they are present and no broader than described:

# cluster-scoped: read-only, restricted to the single 'cluster' APIServer object
oc get clusterrole netobserv-cli -o yaml | grep -A8 config.openshift.io

# namespaced: write the resolved ConfigMap in the run namespace only
oc -n netobserv-cli get role netobserv-cli -o yaml
oc -n netobserv-cli get rolebinding netobserv-cli -o yaml

Expected: get on apiservers limited via resourceNames: [cluster], and get/create/update on configmaps scoped to the run namespace (no delete, no cluster-wide ConfigMap access).

Dependencies

n/a

Checklist

  • Does the changes in PR need specific configuration or environment set up for testing?
    • if so please describe it in PR description.
  • I have added thorough unit tests for the change.
  • QE requirements (check 1 from the list):
  • Standard QE validation, with pre-merge tests unless stated otherwise.
  • Regression tests only (e.g. refactoring with no user-facing change).
  • No QE (e.g. trivial change with high reviewer's confidence, or per agreement with the QE team).

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the openshift-eng/jira-lifecycle-plugin repository.

@openshift-ci

openshift-ci Bot commented Sep 15, 2026

Copy link
Copy Markdown

@leandroberetta: The following test failed, say /retest to rerun all failed tests or /retest-required to rerun all mandatory failed tests:

Test name Commit Details Required Rerun command
ci/prow/integration-tests 3205dfa link true /test integration-tests

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@kapjain-rh

Copy link
Copy Markdown
Member

/label qe-approve

Tested and its behavior depend upon the when the node reconcile after policy change
tested all profiles and around cases and working fine

@openshift-ci

openshift-ci Bot commented Sep 16, 2026

Copy link
Copy Markdown

@kapjain-rh: The label(s) `/label qe-approve

cannot be applied. These labels are supported:acknowledge-critical-fixes-only, platform/aws, platform/azure, platform/baremetal, platform/google, platform/libvirt, platform/openstack, ga, tide/merge-method-merge, tide/merge-method-rebase, tide/merge-method-squash, px-approved, docs-approved, qe-approved, ux-approved, no-qe, rebase/manual, cluster-config-api-changed, run-integration-tests, verified, ready-for-human-review, reliability, approved, backport-risk-assessed, bugzilla/valid-bug, cherry-pick-approved, ci/severity-critical, jira/skip-dependent-bug-check, jira/valid-bug, ok-to-test, stability-fix-approved, staff-eng-approved. Is this label configured under labels -> additional_labelsorlabels -> restricted_labelsinplugin.yaml`?

Details

In response to this:

/label qe-approve

Tested and its behavior depend upon the when the node reconcile after policy change
tested all profiles and around cases and working fine

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository.

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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants