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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
The diff you're trying to view is too large. We only load the first 3000 changed files.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
extractor/target/
runtime-samples/**/target/
# If you prefer the allow list template instead of the deny list, see community template:
# https://github.com/github/gitignore/blob/main/community/Golang/Go.AllowList.gitignore
#
Expand Down Expand Up @@ -32,7 +34,6 @@ out-*/
.svn
# ignore Maven generated target folders
~
target
# ignore downloaded maven
/tools/maven
/tools/maven.zip
Expand Down
2 changes: 1 addition & 1 deletion Containerfile-extractor
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ FROM registry.ci.openshift.org/ocp/4.22:base-rhel9 AS rust-builder
ARG TARGETARCH

RUN dnf update -y && \
dnf -y install gcc make wget rust-toolset rustfmt && \
dnf -y install gcc make wget rust-toolset rustfmt openssl-devel perl && \
dnf clean all && \
rm -rf /var/cache/dnf/*
WORKDIR /workspace/extractor
Expand Down
411 changes: 411 additions & 0 deletions docs/prd/prd-0002-tls-profile-compliance.md

Large diffs are not rendered by default.

205 changes: 190 additions & 15 deletions exporter/cmd/exporter/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,27 @@ package main

import (
"bufio"
"context"
"crypto/sha256"
"crypto/tls"
"crypto/x509"
"encoding/json"
"flag"
"fmt"
"log"
"net"
"net/http"
"os"
"path/filepath"
"strings"
"time"

configv1 "github.com/openshift/api/config/v1"
configclientset "github.com/openshift/client-go/config/clientset/versioned"
"k8s.io/apimachinery/pkg/api/equality"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/watch"
"k8s.io/client-go/rest"

"exporter/pkg/types"
"exporter/pkg/utils"
)
Expand All @@ -22,6 +31,9 @@ const (
EXTRACTOR_ADDRESS string = "127.0.0.1:3000"
)

var extractorTLSCA string
var extractorTLSServerName string

// GatherRuntimeInfoRequest represents the JSON body for POST requests
type GatherRuntimeInfoRequest struct {
ContainerIds []string `json:"containerIds"`
Expand Down Expand Up @@ -87,28 +99,35 @@ func gatherRuntimeInfo(w http.ResponseWriter, r *http.Request) {
}

func triggerRuntimeInfoExtraction(containerIds []string) (string, error) {
conn, err := net.Dial("tcp", EXTRACTOR_ADDRESS)
caCert, err := os.ReadFile(extractorTLSCA)
if err != nil {
return "", fmt.Errorf("failed to read CA certificate: %w", err)
}
caCertPool := x509.NewCertPool()
if !caCertPool.AppendCertsFromPEM(caCert) {
return "", fmt.Errorf("failed to parse CA certificate")
}

tlsConfig := &tls.Config{
RootCAs: caCertPool,
ServerName: extractorTLSServerName,
}

conn, err := tls.Dial("tcp", EXTRACTOR_ADDRESS, tlsConfig)
if err != nil {
return "", err
}
defer conn.Close()

log.Println("Requesting a new runtime extraction")
tcpConn, ok := conn.(*net.TCPConn)
if !ok {
return "", fmt.Errorf("failed to get TCP connection")
}

// Send comma-separated container IDs (empty string if no specific containers requested)
payload := strings.Join(containerIds, ",")
// write to TCP connection to trigger a runtime extraction
fmt.Fprintf(tcpConn, "%s", payload)
// and close the write side to signal EOF to the server
if err := tcpConn.CloseWrite(); err != nil {
fmt.Fprintf(conn, "%s", payload)
if err := conn.CloseWrite(); err != nil {
return "", fmt.Errorf("failed to close write side: %w", err)
}

dataPath, err := bufio.NewReader(tcpConn).ReadString('\n')
dataPath, err := bufio.NewReader(conn).ReadString('\n')
if err != nil {
return "", err
}
Expand Down Expand Up @@ -189,16 +208,172 @@ func collectWorkloadPayload(hash bool, dataPath string) (types.NodeRuntimeInfo,
return payload, nil
}

// buildTLSConfigFromProfile builds a tls.Config from the OpenShift TLS security profile
func buildTLSConfigFromProfile(profile *configv1.TLSSecurityProfile) (*tls.Config, error) {
spec, err := resolveProfileSpec(profile)
if err != nil {
return nil, err
}

minVer, err := parseTLSVersion(string(spec.MinTLSVersion))
if err != nil {
return nil, fmt.Errorf("invalid MinTLSVersion: %w", err)
}

cfg := &tls.Config{MinVersion: minVer}

// TLS 1.3 cipher suites are not configurable in Go (golang/go#29349)
if minVer < tls.VersionTLS13 {
suites := mapCipherSuites(spec.Ciphers)
if len(suites) == 0 {
return nil, fmt.Errorf("no valid cipher suites found in TLS profile")
}
cfg.CipherSuites = suites
}

return cfg, nil
}

func resolveProfileSpec(profile *configv1.TLSSecurityProfile) (*configv1.TLSProfileSpec, error) {
if profile == nil {
return configv1.TLSProfiles[configv1.TLSProfileIntermediateType], nil
}

switch profile.Type {
case configv1.TLSProfileOldType,
configv1.TLSProfileIntermediateType,
configv1.TLSProfileModernType:
return configv1.TLSProfiles[profile.Type], nil
case configv1.TLSProfileCustomType:
if profile.Custom == nil {
return nil, fmt.Errorf("custom profile specified but Custom is nil")
}
return &profile.Custom.TLSProfileSpec, nil
default:
return configv1.TLSProfiles[configv1.TLSProfileIntermediateType], nil
}
}

func parseTLSVersion(v string) (uint16, error) {
versions := map[string]uint16{
"VersionTLS10": tls.VersionTLS10,
"VersionTLS11": tls.VersionTLS11,
"VersionTLS12": tls.VersionTLS12,
"VersionTLS13": tls.VersionTLS13,
}
if ver, ok := versions[v]; ok {
return ver, nil
}
return 0, fmt.Errorf("unknown TLS version: %s", v)
}

// mapCipherSuites converts OpenSSL-style cipher names to Go crypto/tls constants
func mapCipherSuites(names []string) []uint16 {
m := map[string]uint16{
"ECDHE-RSA-AES128-GCM-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256,
"ECDHE-ECDSA-AES128-GCM-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256,
"ECDHE-RSA-AES256-GCM-SHA384": tls.TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384,
"ECDHE-ECDSA-AES256-GCM-SHA384": tls.TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384,
"ECDHE-RSA-CHACHA20-POLY1305": tls.TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305,
"ECDHE-ECDSA-CHACHA20-POLY1305": tls.TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305,
"ECDHE-RSA-AES128-SHA256": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA256,
"ECDHE-ECDSA-AES128-SHA256": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA256,
"ECDHE-RSA-AES128-SHA": tls.TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA,
"ECDHE-ECDSA-AES128-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA,
"ECDHE-RSA-AES256-SHA": tls.TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA,
"ECDHE-ECDSA-AES256-SHA": tls.TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA,
"AES128-GCM-SHA256": tls.TLS_RSA_WITH_AES_128_GCM_SHA256,
"AES256-GCM-SHA384": tls.TLS_RSA_WITH_AES_256_GCM_SHA384,
"AES128-SHA256": tls.TLS_RSA_WITH_AES_128_CBC_SHA256,
"AES128-SHA": tls.TLS_RSA_WITH_AES_128_CBC_SHA,
"AES256-SHA": tls.TLS_RSA_WITH_AES_256_CBC_SHA,
"DES-CBC3-SHA": tls.TLS_RSA_WITH_3DES_EDE_CBC_SHA,
}

out := make([]uint16, 0, len(names))
for _, name := range names {
if id, ok := m[name]; ok {
out = append(out, id)
}
}
return out
}

// watchAndExitOnTLSChange watches for TLS profile changes and exits the process
func watchAndExitOnTLSChange(configClient configclientset.Interface, current *configv1.TLSSecurityProfile) {
w, err := configClient.ConfigV1().APIServers().Watch(context.Background(), metav1.ListOptions{
FieldSelector: "metadata.name=cluster",
})
if err != nil {
log.Printf("Failed to watch APIServer for TLS changes: %v", err)
return
}
defer w.Stop()

for event := range w.ResultChan() {
if event.Type != watch.Modified {
continue
}
updated, ok := event.Object.(*configv1.APIServer)
if !ok {
continue
}
if !equality.Semantic.DeepEqual(current, updated.Spec.TLSSecurityProfile) {
log.Println("TLS profile changed, exiting for restart")
os.Exit(0)
}
}
}

func main() {
bindAddress := flag.String("bind", "127.0.0.1", "Bind address")
bindAddress := flag.String("bind", "0.0.0.0", "Bind address")
tlsCert := flag.String("tls-cert", "", "Path to TLS certificate file (PEM format)")
tlsKey := flag.String("tls-key", "", "Path to TLS private key file (PEM format)")
tlsCAFile := flag.String("tls-ca", "", "Path to CA certificate for extractor TLS verification")
tlsServerName := flag.String("tls-server-name", "", "Server name for extractor TLS certificate verification")

flag.Parse()

if *tlsCert == "" || *tlsKey == "" || *tlsCAFile == "" || *tlsServerName == "" {
log.Fatal("--tls-cert, --tls-key, --tls-ca, and --tls-server-name flags are required")
}

extractorTLSCA = *tlsCAFile
extractorTLSServerName = *tlsServerName

// Fetch TLS profile from API Server
restConfig, err := rest.InClusterConfig()
if err != nil {
log.Fatalf("Failed to get in-cluster config: %v", err)
}
configClient, err := configclientset.NewForConfig(restConfig)
if err != nil {
log.Fatalf("Failed to create config client: %v", err)
}

apiserver, err := configClient.ConfigV1().APIServers().Get(context.Background(), "cluster", metav1.GetOptions{})
if err != nil {
log.Fatalf("Failed to get APIServer config: %v", err)
}

tlsConfig, err := buildTLSConfigFromProfile(apiserver.Spec.TLSSecurityProfile)
if err != nil {
log.Fatalf("Failed to build TLS config: %v", err)
}

http.HandleFunc("/gather_runtime_info", gatherRuntimeInfo)

address := *bindAddress + ":8000"
log.Printf("Starting exporter HTTP server at %s\n", address)
if err := http.ListenAndServe(address, nil); err != nil {
server := &http.Server{
Addr: address,
TLSConfig: tlsConfig,
}

// Watch for TLS profile changes in background
go watchAndExitOnTLSChange(configClient, apiserver.Spec.TLSSecurityProfile)

log.Printf("Starting exporter HTTPS server at %s\n", address)
if err := server.ListenAndServeTLS(*tlsCert, *tlsKey); err != nil {
log.Fatal(err)
}
}
2 changes: 2 additions & 0 deletions exporter/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ go 1.25.3

require (
github.com/onsi/gomega v1.39.1
github.com/openshift/api v0.0.0-20260329113031-1e7cd4b531e7
github.com/openshift/client-go v0.0.0-20260320040014-4b5fc2cdad98
github.com/stretchr/testify v1.11.1
k8s.io/api v0.35.1
k8s.io/apimachinery v0.35.1
Expand Down
4 changes: 4 additions & 0 deletions exporter/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,10 @@ github.com/onsi/ginkgo/v2 v2.28.0 h1:Rrf+lVLmtlBIKv6KrIGJCjyY8N36vDVcutbGJkyqjJc
github.com/onsi/ginkgo/v2 v2.28.0/go.mod h1:ArE1D/XhNXBXCBkKOLkbsb2c81dQHCRcF5zwn/ykDRo=
github.com/onsi/gomega v1.39.1 h1:1IJLAad4zjPn2PsnhH70V4DKRFlrCzGBNrNaru+Vf28=
github.com/onsi/gomega v1.39.1/go.mod h1:hL6yVALoTOxeWudERyfppUcZXjMwIMLnuSfruD2lcfg=
github.com/openshift/api v0.0.0-20260329113031-1e7cd4b531e7 h1:x/t7qluHNogZ3UBaLe2xGx+gFQeMdNOoYxCKMFC/vWg=
github.com/openshift/api v0.0.0-20260329113031-1e7cd4b531e7/go.mod h1:pyVjK0nZ4sRs4fuQVQ4rubsJdahI1PB94LnQ8sGdvxo=
github.com/openshift/client-go v0.0.0-20260320040014-4b5fc2cdad98 h1:Ssuo/zELWqb7pFCwzB3QGEA4QeLW948hL2AhWq2SWjs=
github.com/openshift/client-go v0.0.0-20260320040014-4b5fc2cdad98/go.mod h1:8O4jIKdcr5YR9FFQEeokYoCplCUN+j9hZj4u/2yg0As=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
Expand Down
Loading