Skip to content

Add ALPN/NextProtos helpers for TLS config - #21

Open
ugiordan wants to merge 3 commits into
openshift:mainfrom
ugiordan:add-alpn-nextprotos-helpers
Open

Add ALPN/NextProtos helpers for TLS config#21
ugiordan wants to merge 3 commits into
openshift:mainfrom
ugiordan:add-alpn-nextprotos-helpers

Conversation

@ugiordan

@ugiordan ugiordan commented Jun 18, 2026

Copy link
Copy Markdown

Summary

Adds composable ALPN protocol negotiation helpers to pkg/tls, eliminating
copy-paste boilerplate found in 49+ repos across the openshift org.

Currently every operator using NewTLSConfigFromProfile must manually append
a func(*tls.Config) to set tls.Config.NextProtos. This change provides:

  • HTTP2NextProtos / HTTP1NextProtos: well-known ALPN protocol lists
  • SetNextProtos(protos ...string): explicit ALPN setter with defensive copy and empty-string filtering

Before (every operator today):

// Fetch the cluster TLS profile
tlsProfile, err := openshifttls.FetchAPIServerTLSProfile(ctx, client)
if err != nil {
    return fmt.Errorf("failed to get tls profile: %w", err)
}

// Build TLS config from the profile
tlsConfig, unsupported := openshifttls.NewTLSConfigFromProfile(tlsProfile)
if len(unsupported) > 0 {
    setupLog.Info("unsupported ciphers", "ciphers", unsupported)
}

// Manually set ALPN (copy-pasted across 49+ repos)
disableHTTP2 := func(c *tls.Config) {
    setupLog.Info("disabling http/2")
    c.NextProtos = []string{"http/1.1"}
}

// Wire into controller-runtime
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
    Metrics: metricsserver.Options{
        TLSOpts: []func(*tls.Config){tlsConfig, disableHTTP2},
    },
    WebhookServer: &webhook.DefaultServer{Options: webhook.Options{
        TLSOpts: []func(*tls.Config){tlsConfig, disableHTTP2},
    }},
})

After:

// Fetch the cluster TLS profile
tlsProfile, err := openshifttls.FetchAPIServerTLSProfile(ctx, client)
if err != nil {
    return fmt.Errorf("failed to get tls profile: %w", err)
}

// Build TLS config from the profile
tlsConfig, unsupported := openshifttls.NewTLSConfigFromProfile(tlsProfile)
if len(unsupported) > 0 {
    setupLog.Info("unsupported ciphers", "ciphers", unsupported)
}

// Compose profile + ALPN into a single TLSOpts slice
tlsOpts := []func(*tls.Config){tlsConfig, openshifttls.SetNextProtos(openshifttls.HTTP1NextProtos...)}

// Wire into controller-runtime
mgr, err := ctrl.NewManager(cfg, ctrl.Options{
    Metrics: metricsserver.Options{
        TLSOpts: tlsOpts,
    },
    WebhookServer: &webhook.DefaultServer{Options: webhook.Options{
        TLSOpts: tlsOpts,
    }},
})

Other usage patterns:

// Disable HTTP/2 (defense-in-depth for CVE-2023-44487)
openshifttls.SetNextProtos(openshifttls.HTTP1NextProtos...)

// Enable HTTP/2 with HTTP/1.1 fallback
openshifttls.SetNextProtos(openshifttls.HTTP2NextProtos...)

// Inline protocol values
openshifttls.SetNextProtos("h2", "http/1.1")

// Conditional inclusion (empty strings are filtered out)
openshifttls.SetNextProtos(conditionalH2, "http/1.1")

// Use the exported vars directly when building a tls.Config outside TLSOpts
cfg := &tls.Config{
    NextProtos: openshifttls.HTTP1NextProtos,
}

Concrete use-cases

Repos already using NewTLSConfigFromProfile alongside manual NextProtos:

  • openshift/lvm-operator (cmd/operator/operator.go, cmd/vgmanager/vgmanager.go)
  • openshift/ptp-operator (main.go)
  • openshift/pf-status-relay-operator (cmd/main.go)
  • openshift/vertical-pod-autoscaler-operator (cmd/main.go)
  • openshift/cluster-node-tuning-operator (cmd/cluster-node-tuning-operator/main.go)

GitHub code search shows 49 total repos in the openshift org with this pattern.

Design decisions

  • Separate from NewTLSConfigFromProfile: NextProtos is HTTP protocol
    negotiation, not TLS cipher/version selection. Different consumers want
    different values. Composable functions preserve the existing API.
  • Explicit over opinionated: callers directly control the protocol list
    they're setting rather than going through a boolean abstraction.
  • Defensive copy in SetNextProtos: prevents callers from mutating the
    captured slice.
  • Empty-string filtering: allows conditional protocol inclusion without
    requiring wrapper functions.
  • nil preservation: SetNextProtos() with zero non-empty args returns nil
    to preserve tls.Config default semantics.
  • Follows existing patterns: func(*tls.Config) return type, exported
    package-level vars with nolint:gochecknoglobals, Ginkgo/Gomega tests.

Verification

  • make test passes (all tests green with race detector)
  • make lint passes (0 issues)

ugiordan and others added 2 commits June 18, 2026 12:08
Add SetNextProtos and WithHTTP2 composable functions that follow the
existing func(*tls.Config) pattern used by controller-runtime's TLSOpts.

Currently every operator using NewTLSConfigFromProfile must manually
append a function to set tls.Config.NextProtos for ALPN negotiation.
This is copy-pasted across 49+ repos in the openshift org, typically
as a CVE-2023-44487 mitigation (disabling HTTP/2).

These helpers eliminate that boilerplate:

  // Before:
  disableHTTP2 := func(c *tls.Config) {
      c.NextProtos = []string{"http/1.1"}
  }
  tlsOpts := []func(*tls.Config){tlsConfig, disableHTTP2}

  // After:
  tlsOpts := []func(*tls.Config){tlsConfig, openshifttls.WithHTTP2(false)}

New exports:
- HTTP2NextProtos: []string{"h2", "http/1.1"}
- HTTP1NextProtos: []string{"http/1.1"}
- SetNextProtos(protos ...string): generic ALPN setter
- WithHTTP2(enabled bool): convenience for HTTP/2 enable/disable

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1. Reword HTTP1NextProtos comment to clarify this is defense-in-depth
   against CVE-2023-44487/CVE-2023-39325, not a standalone fix. The
   primary fixes are in Go 1.21.3+ and golang.org/x/net v0.17.0+.

2. Fix SetNextProtos to preserve nil semantics when called with zero
   arguments. Previously make([]string, 0) produced a non-nil empty
   slice, which has different TLS behavior than nil (nil = no ALPN
   negotiation, empty = ALPN extension with no protocols).

3. Update test assertion to use BeNil() instead of BeEmpty() to
   verify the nil vs empty distinction.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@openshift-ci
openshift-ci Bot requested review from joelanford and sdodson June 18, 2026 11:31
@ugiordan

ugiordan commented Jun 23, 2026

Copy link
Copy Markdown
Author

@sdodson @joelanford would appreciate a review when you get a chance. This adds ALPN/NextProtos to the TLS config returned by NewTLSConfigFromProfile. Currently every operator using this library has to append NextProtos manually. This fixes it at the library level.

Comment thread pkg/tls/tls.go Outdated
Address review feedback from joelanford: WithHTTP2 is too opinionated
for a shared library. It silently replaces the entire NextProtos list,
which doesn't compose well when controller-runtime or other callers
already have protos set.

Changes:
- Remove WithHTTP2 function entirely
- Add empty-string filtering to SetNextProtos so callers can use
  conditional inclusion patterns without wrapper functions
- Update godoc examples to show the well-known protocol list vars
- Replace WithHTTP2 tests with empty-string filtering and
  composability tests

Callers are now explicit about what they're setting:

  SetNextProtos(openshifttls.HTTP1NextProtos...)
  SetNextProtos(openshifttls.HTTP2NextProtos...)
  SetNextProtos("h2", "http/1.1")

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@openshift-ci

openshift-ci Bot commented Jun 25, 2026

Copy link
Copy Markdown

@ugiordan: all tests passed!

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.

@joelanford

Copy link
Copy Markdown
Member

/approve

@openshift-ci

openshift-ci Bot commented Jun 30, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: joelanford, ugiordan

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

The pull request process is described 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 openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jun 30, 2026
@ugiordan

ugiordan commented Jun 30, 2026

Copy link
Copy Markdown
Author

@joelanford can I get a lgtm?

@ugiordan
ugiordan requested a review from joelanford July 7, 2026 08:47
@ugiordan

Copy link
Copy Markdown
Author

@joelanford could you please help with this one?

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

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants