Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 121 additions & 0 deletions data/data/install.openshift.io_installconfigs.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6138,6 +6138,77 @@ spec:
type: string
maxItems: 2
type: array
bgpVIPConfig:
description: |-
BGPVIPConfig configures BGP-based advertisement of the API and
Ingress VIPs. When set, kube-vip (Routing Table Mode) and frr-k8s
are deployed as static pods on control plane nodes to advertise
VIPs via BGP, replacing the default keepalived/VRRP mechanism.
properties:
communities:
description: |-
Communities is an optional list of BGP communities to attach to
advertised VIP routes, in the format "ASN:value" (e.g., "64512:100").
items:
type: string
type: array
localASN:
description: LocalASN is the Autonomous System Number for
this cluster's BGP speaker.
format: int64
type: integer
peers:
description: Peers is the list of BGP peers to advertise VIPs
to.
items:
description: BGPPeerConfig defines the configuration for
a BGP peer.
properties:
bfdEnabled:
description: |-
BFDEnabled configures Bi-directional Forwarding Detection.
Valid values are "true" and "false".
type: string
ebgpMultiHop:
description: EBGPMultiHop enables multi-hop eBGP when
the peer is not directly connected.
type: string
holdTime:
description: HoldTime is the BGP hold time for this
peer (e.g., "90s").
type: string
keepaliveTime:
description: KeepaliveTime is the BGP keepalive interval
for this peer (e.g., "30s").
type: string
password:
description: |-
Password is the optional TCP MD5 signature password for the BGP
session (RFC 2385), consumed by FRR.
type: string
peerASN:
description: PeerASN is the Autonomous System Number
of the BGP peer.
format: int64
type: integer
peerAddress:
description: PeerAddress is the IP address of the BGP
peer (e.g., ToR switch).
type: string
port:
description: Port is the TCP port for the BGP session.
Defaults to 179.
format: int32
type: integer
required:
- peerASN
- peerAddress
type: object
type: array
required:
- localASN
- peers
type: object
bmcVerifyCA:
type: string
bootstrapExternalStaticDNS:
Expand Down Expand Up @@ -6227,6 +6298,56 @@ spec:
description: Host stores all the configuration data for a baremetal
host.
properties:
bgpPeers:
description: |-
BGPPeers overrides the global bgpVIPConfig.peers for this specific
host. When set, this host will peer with the listed BGP peers instead
of the global peer list.
items:
description: BGPPeerConfig defines the configuration for
a BGP peer.
properties:
bfdEnabled:
description: |-
BFDEnabled configures Bi-directional Forwarding Detection.
Valid values are "true" and "false".
type: string
ebgpMultiHop:
description: EBGPMultiHop enables multi-hop eBGP when
the peer is not directly connected.
type: string
holdTime:
description: HoldTime is the BGP hold time for this
peer (e.g., "90s").
type: string
keepaliveTime:
description: KeepaliveTime is the BGP keepalive interval
for this peer (e.g., "30s").
type: string
password:
description: |-
Password is the optional TCP MD5 signature password for the BGP
session (RFC 2385), consumed by FRR.
type: string
peerASN:
description: PeerASN is the Autonomous System Number
of the BGP peer.
format: int64
type: integer
peerAddress:
description: PeerAddress is the IP address of the
BGP peer (e.g., ToR switch).
type: string
port:
description: Port is the TCP port for the BGP session.
Defaults to 179.
format: int32
type: integer
required:
- peerASN
- peerAddress
type: object
type: array
bmc:
description: BMC stores the information about a baremetal
host's management controller.
Expand Down
137 changes: 137 additions & 0 deletions pkg/asset/manifests/bgpvipconfig.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
package manifests

import (
"context"
"encoding/json"
"path"

"github.com/pkg/errors"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"sigs.k8s.io/yaml"

"github.com/openshift/installer/pkg/asset"
"github.com/openshift/installer/pkg/asset/installconfig"
"github.com/openshift/installer/pkg/types/baremetal"
)

var (
bgpVIPConfigMapFileName = path.Join(manifestDir, "bgp-vip-config.yaml")
)

const (
bgpVIPConfigMapName = "bgp-vip-config"
bgpVIPConfigMapNamespace = "openshift-network-operator"
bgpVIPConfigMapDataKey = "config.json"
)

// bgpVIPConfigJSON is the JSON structure stored in the ConfigMap. Its field
// names match baremetal-runtimecfg's FRRPeerMapping so the same JSON can be
// written verbatim to /etc/kubernetes/static-pod-resources/frr-k8s/frr-peers.json.
type bgpVIPConfigJSON struct {
LocalASN int64 `json:"localASN"`
DefaultPeers []baremetal.BGPPeerConfig `json:"defaultPeers"`
Communities []string `json:"communities,omitempty"`
APIVIPs []string `json:"apiVIPs"`
IngressVIPs []string `json:"ingressVIPs"`
HostOverrides map[string][]baremetal.BGPPeerConfig `json:"hostOverrides,omitempty"`
Comment on lines +31 to +37

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Known DevPreview limitation, deliberate for now: the password is BGP TCP-MD5 session authentication (RFC 2385) — the hash is mandated by the BGP protocol option itself, not a choice of ours — and the whole config flows installer → ConfigMap → MCO/CNO as one schema. Moving it to a Secret requires the full chain (installer generates the Secret, MCO bootstrap render and CNO FRRConfiguration consume passwordSecret references) and is planned alongside the TechPreview structured API; the CNO side documents the same limitation (cluster-network-operator#3047, docs/bgp_vip_management.md). Splitting just the installer side now would leave mixed sources without removing the exposure.

This reply was written with AI assistance. Please verify before acting on it.

}

// BGPVIPConfigMap generates the bgp-vip-config ConfigMap for CNO.
type BGPVIPConfigMap struct {
ConfigMap *corev1.ConfigMap
File *asset.File
}

var _ asset.WritableAsset = (*BGPVIPConfigMap)(nil)

// Name returns a human friendly name for the asset.
func (*BGPVIPConfigMap) Name() string {
return "BGP VIP Config ConfigMap"
}

// Dependencies returns all of the dependencies directly needed to generate
// the asset.
func (*BGPVIPConfigMap) Dependencies() []asset.Asset {
return []asset.Asset{
&installconfig.InstallConfig{},
}
}

// Generate generates the BGP VIP Config ConfigMap.
func (b *BGPVIPConfigMap) Generate(_ context.Context, dependencies asset.Parents) error {
installConfig := &installconfig.InstallConfig{}
dependencies.Get(installConfig)

// Only generate for baremetal platform with BGPVIPConfig set.
if installConfig.Config.Platform.Name() != baremetal.Name ||
installConfig.Config.Platform.BareMetal == nil ||
installConfig.Config.Platform.BareMetal.BGPVIPConfig == nil {
return nil
}

bm := installConfig.Config.Platform.BareMetal
bgpConfig := bm.BGPVIPConfig

configData := bgpVIPConfigJSON{
LocalASN: bgpConfig.LocalASN,
DefaultPeers: bgpConfig.Peers,
Communities: bgpConfig.Communities,
APIVIPs: bm.APIVIPs,
IngressVIPs: bm.IngressVIPs,
}

// Collect per-host BGP peer overrides.
hostOverrides := make(map[string][]baremetal.BGPPeerConfig)
for _, host := range bm.Hosts {
if host != nil && len(host.BGPPeers) > 0 {
hostOverrides[host.Name] = host.BGPPeers
}
}
if len(hostOverrides) > 0 {
configData.HostOverrides = hostOverrides
}

jsonBytes, err := json.Marshal(configData)
if err != nil {
return errors.Wrap(err, "failed to marshal BGP VIP config JSON")
}

cm := &corev1.ConfigMap{
TypeMeta: metav1.TypeMeta{
APIVersion: corev1.SchemeGroupVersion.String(),
Kind: "ConfigMap",
},
ObjectMeta: metav1.ObjectMeta{
Namespace: bgpVIPConfigMapNamespace,
Name: bgpVIPConfigMapName,
},
Data: map[string]string{
bgpVIPConfigMapDataKey: string(jsonBytes),
Comment on lines +76 to +110

@coderabbitai coderabbitai Bot Jul 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Do not render BGP credentials into a ConfigMap.

BGPPeerConfig.Password is marshaled into config.json, then published in a ConfigMap. This exposes BGP session credentials in rendered manifests and implements the newly added MD5-based authentication model. Use a secret-backed mechanism and avoid MD5 authentication material.

As per coding guidelines, flag MD5 usage.

🤖 Prompt for 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.

In `@pkg/asset/manifests/bgpvipconfig.go` around lines 76 - 110, Update the BGP
VIP manifest generation around bgpVIPConfigJSON and hostOverrides so
BGPPeerConfig.Password is never serialized into config.json or stored in the
ConfigMap. Use the existing secret-backed mechanism for any required BGP peer
credentials, and remove or avoid MD5-based authentication material; flag any
remaining MD5 usage according to project guidelines.

Source: Coding guidelines

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Known DevPreview limitation, deliberate for now: the password is BGP TCP-MD5 session authentication (RFC 2385) — the hash is mandated by the BGP protocol option itself, not a choice of ours — and the whole config flows installer → ConfigMap → MCO/CNO as one schema. Moving it to a Secret requires the full chain (installer generates the Secret, MCO bootstrap render and CNO FRRConfiguration consume passwordSecret references) and is planned alongside the TechPreview structured API; the CNO side documents the same limitation (cluster-network-operator#3047, docs/bgp_vip_management.md). Splitting just the installer side now would leave mixed sources without removing the exposure.

This reply was written with AI assistance. Please verify before acting on it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '=== Relevant installer symbols ==='
ast-grep outline pkg/types/baremetal/platform.go --items all --match 'BGPPeerConfig|BGPVIPConfig|Host|Platform' --view expanded
ast-grep outline pkg/asset/manifests/bgpvipconfig.go --items all --match 'bgpVIPConfigJSON|BGPVIPConfigMap' --view expanded

printf '%s\n' '=== BGP fields and serialization ==='
rg -n -A18 -B4 'type BGPPeerConfig|type BGPVIPConfig|BGPPeers|BGPVIPConfig|json\.Marshal|hostOverrides' \
  pkg/types/baremetal/platform.go pkg/asset/manifests/bgpvipconfig.go pkg/asset/manifests/bgpvipconfig_test.go

printf '%s\n' '=== In-repository BGP VIP documentation/references ==='
rg -n -i -C3 'BGPBasedVIPManagement|BGP VIP|bgpVIPConfig|passwordSecret|TCP-MD5|RFC 2385|bgp_vip_management' \
  README.md docs pkg data cmd 2>/dev/null || true

printf '%s\n' '=== Referenced CNO item metadata ==='
gh api repos/openshift/cluster-network-operator/issues/3047 \
  --jq '{number, title, state, body, html_url, labels: [.labels[].name]}' || true

printf '%s\n' '=== Referenced CNO item comments containing credential/schema terms ==='
gh api --paginate repos/openshift/cluster-network-operator/issues/3047/comments \
  --jq '.[] | select((.body | ascii_downcase) | test("password|secret|md5|bgp-vip|frrconfiguration")) | {user: .user.login, created_at, body, html_url}' || true

Length of output: 104813


@mkowalski, verified. BGPPeerConfig.Password is currently serialized end-to-end through bgp-vip-config, and the referenced CNO change consumes that same schema for BGP session configuration. There is no compatible secret-reference field in this branch, so removing the field only in installer would break the feature without eliminating the broader exposure.

I also correct the MD5 wording: installer does not implement MD5; this is the BGP TCP-MD5 shared secret required by the protocol.

Given the DevPreview scope and coupled consumer changes, I won’t require a partial installer-only change. The plaintext ConfigMap exposure remains a known pre-GA security limitation and should stay explicitly documented and tracked for the Secret-backed structured API work. Would you like a dedicated follow-up issue created for that end-to-end migration?


✏️ Learnings added
Learnt from: mkowalski
Repo: openshift/installer PR: 10718
File: pkg/asset/manifests/bgpvipconfig.go:76-110
Timestamp: 2026-07-28T07:57:44.312Z
Learning: For BareMetal BGP VIP management, `pkg/asset/manifests/bgpvipconfig.go` intentionally serializes `baremetal.BGPPeerConfig.Password` into the `bgp-vip-config` ConfigMap as a DevPreview limitation. The current installer → CNO → baremetal-runtimecfg schema consumes inline BGP TCP-MD5 session secrets, and an installer-only Secret migration would break that coupled schema. A Secret-backed design requires coordinated installer, MCO bootstrap/static-pod, and CNO `FRRConfiguration` support and is planned with the TechPreview structured API.

Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.

},
}

cmData, err := yaml.Marshal(cm)
if err != nil {
return errors.Wrapf(err, "failed to create %s manifest", b.Name())
}
b.ConfigMap = cm
b.File = &asset.File{
Filename: bgpVIPConfigMapFileName,
Data: cmData,
}
return nil
}

// Files returns the files generated by the asset.
func (b *BGPVIPConfigMap) Files() []*asset.File {
if b.File != nil {
return []*asset.File{b.File}
}
return []*asset.File{}
}

// Load loads the already-rendered files back from disk.
func (b *BGPVIPConfigMap) Load(f asset.FileFetcher) (bool, error) {
return false, nil
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
72 changes: 72 additions & 0 deletions pkg/asset/manifests/bgpvipconfig_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package manifests

import (
"context"
"encoding/json"
"testing"

"github.com/stretchr/testify/assert"

"github.com/openshift/installer/pkg/asset"
"github.com/openshift/installer/pkg/asset/installconfig"
"github.com/openshift/installer/pkg/types"
"github.com/openshift/installer/pkg/types/baremetal"
)

func bgpInstallConfig() *installconfig.InstallConfig {
return installconfig.MakeAsset(&types.InstallConfig{
Platform: types.Platform{
BareMetal: &baremetal.Platform{
APIVIPs: []string{"192.168.111.5"},
IngressVIPs: []string{"192.168.111.4"},
BGPVIPConfig: &baremetal.BGPVIPConfig{
LocalASN: 64512,
Peers: []baremetal.BGPPeerConfig{
{PeerAddress: "192.168.111.1", PeerASN: 64513},
},
},
Hosts: []*baremetal.Host{
{
Name: "master-0",
BGPPeers: []baremetal.BGPPeerConfig{
{PeerAddress: "192.168.1.1", PeerASN: 64513},
},
},
},
},
},
})
}

// The config.json schema must match baremetal-runtimecfg's FRRPeerMapping:
// top-level "defaultPeers" (not "peers") and hostOverrides as a flat
// map[hostname][]peer (not map[hostname]{"peers":[...]}).
func TestBGPVIPConfigMapSchemaMatchesRuntimecfg(t *testing.T) {
parents := asset.Parents{}
parents.Add(bgpInstallConfig())

cmAsset := &BGPVIPConfigMap{}
if !assert.NoError(t, cmAsset.Generate(context.Background(), parents)) {
return
}
if !assert.NotNil(t, cmAsset.ConfigMap) {
return
}

var got map[string]json.RawMessage
if !assert.NoError(t, json.Unmarshal([]byte(cmAsset.ConfigMap.Data["config.json"]), &got)) {
return
}

assert.Contains(t, got, "defaultPeers")
assert.NotContains(t, got, "peers")

var overrides map[string][]baremetal.BGPPeerConfig
if !assert.NoError(t, json.Unmarshal(got["hostOverrides"], &overrides)) {
return
}
if !assert.Len(t, overrides["master-0"], 1) {
return
}
assert.Equal(t, "192.168.1.1", overrides["master-0"][0].PeerAddress)
}
3 changes: 3 additions & 0 deletions pkg/asset/manifests/infrastructure.go
Original file line number Diff line number Diff line change
Expand Up @@ -192,6 +192,9 @@ func (i *Infrastructure) Generate(ctx context.Context, dependencies asset.Parent
config.Spec.PlatformSpec.BareMetal.IngressIPs = types.StringsToIPs(installConfig.Config.Platform.BareMetal.IngressVIPs)
config.Spec.PlatformSpec.BareMetal.MachineNetworks = types.MachineNetworksToCIDRs(installConfig.Config.MachineNetwork)
config.Status.PlatformStatus.BareMetal.MachineNetworks = types.MachineNetworksToCIDRs(installConfig.Config.MachineNetwork)
if installConfig.Config.Platform.BareMetal.BGPVIPConfig != nil {
config.Status.PlatformStatus.BareMetal.VIPManagement = configv1.VIPManagementTypeBGP
}
case gcp.Name:
config.Spec.PlatformSpec.Type = configv1.GCPPlatformType
config.Status.PlatformStatus.GCP = &configv1.GCPPlatformStatus{
Expand Down
11 changes: 11 additions & 0 deletions pkg/asset/manifests/network.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"github.com/openshift/installer/pkg/asset/installconfig"
"github.com/openshift/installer/pkg/types"
"github.com/openshift/installer/pkg/types/aws"
"github.com/openshift/installer/pkg/types/baremetal"
"github.com/openshift/installer/pkg/types/powervs"
)

Expand Down Expand Up @@ -145,6 +146,16 @@ func clusterNetworkOperatorConfig(ic *installconfig.InstallConfig, cns []configv
cnoCfg = ovnNetworkOperatorConfig(cns, sn)
cnoCfg.Spec.DefaultNetwork.OVNKubernetesConfig.GatewayConfig = &operatorv1.GatewayConfig{RoutingViaHost: true}
}
case baremetal.Name:
// BGP-based VIP management needs frr-k8s CRDs/namespace deployed by
// CNO; enable the FRR routing capability provider. Non-OVN network
// types are rejected by install-config validation.
if ic.Config.Platform.BareMetal.BGPVIPConfig != nil && ic.Config.NetworkType == string(operatorv1.NetworkTypeOVNKubernetes) {
cnoCfg = ovnNetworkOperatorConfig(cns, sn)
cnoCfg.Spec.AdditionalRoutingCapabilities = &operatorv1.AdditionalRoutingCapabilities{
Providers: []operatorv1.RoutingCapabilitiesProvider{operatorv1.RoutingCapabilitiesProviderFRR},
}
}
Comment on lines +149 to +158

@coderabbitai coderabbitai Bot Jul 28, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Reject BGP VIP configuration on non-OVN networks.

Line 153 always creates an OVN configuration, whose default network type is OVNKubernetes. A BGP VIP-configured bare-metal install using another network type would silently render an incompatible network manifest. Reject that install-config combination in platform validation and retain an OVN-type guard here; add a regression test.

🤖 Prompt for 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.

In `@pkg/asset/manifests/network.go` around lines 149 - 157, Update platform
validation to reject bare-metal configurations that set BGPVIPConfig when the
selected network type is not OVNKubernetes. In the baremetal branch of the
manifest generation logic around ovnNetworkOperatorConfig, retain an explicit
OVNKubernetes guard before creating the OVN configuration, and add a regression
test covering the rejected combination.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed: install-config validation now rejects bgpVIPConfig with any network type other than OVNKubernetes, and the manifest generator additionally guards the OVN config creation on the network type. Regression test added (TestValidateBGPVIP: 'bgp requires OVNKubernetes').

This reply was written with AI assistance. Please verify before acting on it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '== Candidate files =='
fd -t f . pkg/types/baremetal pkg/asset/manifests | rg '(platform|network).*(_test)?\.go$|featuregates\.go$'

printf '%s\n' '== Validation structure =='
ast-grep outline pkg/types/baremetal/validation --items all --type function --match 'ValidatePlatform|validateBGPVIPConfig|TestValidateBGPVIP' 2>/dev/null || true

printf '%s\n' '== Relevant validation and regression-test references =='
rg -n -C 8 'BGPVIPConfig|OVNKubernetes|bgp requires OVNKubernetes|TestValidateBGPVIP' \
  pkg/types/baremetal/validation pkg/asset/manifests/network.go pkg/asset/manifests/network_test.go

printf '%s\n' '== Manifest guard =='
sed -n '140,170p' pkg/asset/manifests/network.go

Length of output: 31127


@mkowalski, verified on the current PR branch.

  • Validation rejects bgpVIPConfig unless NetworkType is OVNKubernetes.
  • The manifest generator retains the explicit OVN guard before rendering the FRR-enabled CNO configuration.
  • TestValidateBGPVIP includes the bgp requires OVNKubernetes regression case and checks the expected error.

This resolves the incompatible manifest path. Static verification passed; tests were not executed in this environment.

}

if ovnCfg := ic.Config.OVNKubernetesConfig; ovnCfg != nil && ovnCfg.IPv4 != nil && ovnCfg.IPv4.InternalJoinSubnet != nil {
Expand Down
Loading