Skip to content
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

COO-481: feat: require TLS client certificate for /metrics #611

Merged
merged 1 commit into from
Nov 5, 2024
Merged
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
3 changes: 2 additions & 1 deletion cmd/operator/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -120,13 +120,14 @@ func main() {
op, err := operator.New(
ctx,
operator.NewOperatorConfiguration(
operator.WithNamespace(namespace),
operator.WithMetricsAddr(metricsAddr),
operator.WithHealthProbeAddr(healthProbeAddr),
operator.WithPrometheusImage(imgMap["prometheus"]),
operator.WithAlertmanagerImage(imgMap["alertmanager"]),
operator.WithThanosSidecarImage(imgMap["thanos"]),
operator.WithThanosQuerierImage(imgMap["thanos"]),
operator.WithUIPlugins(namespace, imgMap),
operator.WithUIPluginImages(imgMap),
operator.WithFeatureGates(operator.FeatureGates{
OpenShift: operator.OpenShiftFeatureGates{
Enabled: openShiftEnabled,
Expand Down
47 changes: 38 additions & 9 deletions pkg/operator/operator.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@ import (
"path/filepath"
"time"

v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/server/dynamiccertificates"
"k8s.io/client-go/kubernetes"
typedv1 "k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/tools/record"
ctrl "sigs.k8s.io/controller-runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/healthz"
Expand Down Expand Up @@ -40,6 +43,7 @@ const (
type Operator struct {
manager manager.Manager
servingCertController *dynamiccertificates.DynamicServingCertificateController
clientCAController *dynamiccertificates.ConfigMapCAController
}

type OpenShiftFeatureGates struct {
Expand All @@ -51,6 +55,7 @@ type FeatureGates struct {
}

type OperatorConfiguration struct {
Namespace string
MetricsAddr string
HealthProbeAddr string
Prometheus stackctrl.PrometheusConfiguration
Expand All @@ -61,6 +66,13 @@ type OperatorConfiguration struct {
FeatureGates FeatureGates
}

func WithNamespace(ns string) func(*OperatorConfiguration) {
return func(oc *OperatorConfiguration) {
oc.Namespace = ns
oc.UIPlugins.ResourcesNamespace = ns
}
}

func WithPrometheusImage(image string) func(*OperatorConfiguration) {
return func(oc *OperatorConfiguration) {
oc.Prometheus.Image = image
Expand Down Expand Up @@ -97,10 +109,9 @@ func WithHealthProbeAddr(addr string) func(*OperatorConfiguration) {
}
}

func WithUIPlugins(namespace string, images map[string]string) func(*OperatorConfiguration) {
func WithUIPluginImages(images map[string]string) func(*OperatorConfiguration) {
return func(oc *OperatorConfiguration) {
oc.UIPlugins.Images = images
oc.UIPlugins.ResourcesNamespace = namespace
}
}

Expand All @@ -120,12 +131,16 @@ func NewOperatorConfiguration(opts ...func(*OperatorConfiguration)) *OperatorCon

func New(ctx context.Context, cfg *OperatorConfiguration) (*Operator, error) {
restConfig := ctrl.GetConfigOrDie()
scheme := NewScheme(cfg)

metricsOpts := metricsserver.Options{
BindAddress: cfg.MetricsAddr,
}

var servingCertController *dynamiccertificates.DynamicServingCertificateController
var (
clientCAController *dynamiccertificates.ConfigMapCAController
servingCertController *dynamiccertificates.DynamicServingCertificateController
)
if cfg.FeatureGates.OpenShift.Enabled {
// When running in OpenShift, the server uses HTTPS thanks to the
// service CA operator.
Expand Down Expand Up @@ -162,8 +177,7 @@ func New(ctx context.Context, cfg *OperatorConfiguration) (*Operator, error) {
return nil, err
}

// ConfigMapCAController automatically reloads the client CA.
clientCAProvider, err := dynamiccertificates.NewDynamicCAFromConfigMapController(
clientCAController, err = dynamiccertificates.NewDynamicCAFromConfigMapController(
"client-ca",
metav1.NamespaceSystem,
"extension-apiserver-authentication",
Expand All @@ -174,19 +188,29 @@ func New(ctx context.Context, cfg *OperatorConfiguration) (*Operator, error) {
return nil, fmt.Errorf("failed to initialize client CA controller: %w", err)
}

eventBroadcaster := record.NewBroadcaster()
eventBroadcaster.StartStructuredLogging(0)
eventBroadcaster.StartRecordingToSink(&typedv1.EventSinkImpl{Interface: kubeClient.CoreV1().Events("")})
eventRecorder := record.NewEventRecorderAdapter(
eventBroadcaster.NewRecorder(scheme, v1.EventSource{Component: "cluster-observability-operator"}),
)

servingCertController = dynamiccertificates.NewDynamicServingCertificateController(
&tls.Config{
ClientAuth: tls.NoClientCert,
ClientAuth: tls.RequireAndVerifyClientCert,
},
clientCAProvider,
clientCAController,
certKeyProvider,
nil,
nil,
eventRecorder,
)
if err := servingCertController.RunOnce(); err != nil {
return nil, fmt.Errorf("failed to initialize serving certificate controller: %w", err)
}

clientCAController.AddListener(servingCertController)
certKeyProvider.AddListener(servingCertController)

metricsOpts.SecureServing = true
metricsOpts.TLSOpts = []func(*tls.Config){
func(c *tls.Config) {
Expand All @@ -198,7 +222,7 @@ func New(ctx context.Context, cfg *OperatorConfiguration) (*Operator, error) {
mgr, err := ctrl.NewManager(
restConfig,
ctrl.Options{
Scheme: NewScheme(cfg),
Scheme: scheme,
Metrics: metricsOpts,
HealthProbeBindAddress: cfg.HealthProbeAddr,
})
Expand Down Expand Up @@ -235,10 +259,15 @@ func New(ctx context.Context, cfg *OperatorConfiguration) (*Operator, error) {
return &Operator{
manager: mgr,
servingCertController: servingCertController,
clientCAController: clientCAController,
}, nil
}

func (o *Operator) Start(ctx context.Context) error {
if o.clientCAController != nil {
go o.clientCAController.Run(ctx, 1)
}

if o.servingCertController != nil {
go o.servingCertController.Run(1, ctx.Done())
}
Expand Down
10 changes: 9 additions & 1 deletion test/e2e/framework/assertions.go
Original file line number Diff line number Diff line change
Expand Up @@ -345,14 +345,22 @@ func (f *Framework) getPodMetrics(ctx context.Context, pod *v1.Pod, opts ...func
tr.TLSClientConfig = &tls.Config{
ServerName: fmt.Sprintf("observability-operator.%s.svc", pod.Namespace),
RootCAs: f.RootCA,
GetClientCertificate: func(*tls.CertificateRequestInfo) (*tls.Certificate, error) {
fmt.Printf("client cert: %#v\n", f.MetricsClientCert)
return f.MetricsClientCert, nil
},
}

resp, err := (&http.Client{Transport: tr}).Do(req)
if err != nil {
return nil, fmt.Errorf("failed to get a response from /metrics: %w", err)
return nil, fmt.Errorf("failed to get a response from %q: %w", req.URL.String(), err)
}
defer resp.Body.Close()

if resp.StatusCode != 200 {
return nil, fmt.Errorf("invalid status code from %q: got %d", req.URL.String(), resp.StatusCode)
}

return io.ReadAll(resp.Body)
}

Expand Down
28 changes: 28 additions & 0 deletions test/e2e/framework/framework.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package framework
import (
"bytes"
"context"
"crypto/tls"
"crypto/x509"
"fmt"
"net/http"
Expand Down Expand Up @@ -34,6 +35,7 @@ type Framework struct {
Retain bool
IsOpenshiftCluster bool
RootCA *x509.CertPool
MetricsClientCert *tls.Certificate
OperatorNamespace string
}

Expand Down Expand Up @@ -74,6 +76,32 @@ func (f *Framework) Setup() error {
}
f.RootCA = rootCA

// Load the prometheus-k8s TLS client certificate.
var s v1.Secret
key = client.ObjectKey{
Namespace: "openshift-monitoring",
Name: "metrics-client-certs",
}
if err := f.K8sClient.Get(context.Background(), key, &s); err != nil {
return err
}

cert, found := s.Data["tls.crt"]
if !found {
return errors.New("failed to find TLS client certificate")
}

k, found := s.Data["tls.key"]
if !found {
return errors.New("failed to find TLS client key")
}

c, err := tls.X509KeyPair(cert, k)
if err != nil {
return err
}
f.MetricsClientCert = &c

return nil
}

Expand Down