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
2 changes: 2 additions & 0 deletions cmd/catalog/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -84,6 +84,8 @@ func (o *options) run(ctx context.Context, logger *logrus.Logger) error {
listenAndServe, err := server.GetListenAndServeFunc(
server.WithLogger(logger),
server.WithTLS(&o.tlsCertPath, &o.tlsKeyPath, &o.clientCAPath),
server.WithClientCAAuthorization(o.clientCAAuthorization),
server.WithClientCAAllowedCommonNames(o.clientCAAllowedCommonNames),
server.WithKubeConfig(config),
server.WithAPIServerTLSQuerier(apiServerTLSQuerier),
server.WithDebug(o.debug),
Expand Down
34 changes: 21 additions & 13 deletions cmd/catalog/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,19 +13,21 @@ import (
)

type options struct {
kubeconfig string
catalogNamespace string
configMapServerImage string
opmImage string
utilImage string
writeStatusName string
debug bool
version bool
profiling bool
tlsKeyPath string
tlsCertPath string
clientCAPath string
setWorkloadUserID bool
kubeconfig string
catalogNamespace string
configMapServerImage string
opmImage string
utilImage string
writeStatusName string
debug bool
version bool
profiling bool
tlsKeyPath string
tlsCertPath string
clientCAPath string
clientCAAuthorization bool
clientCAAllowedCommonNames []string
setWorkloadUserID bool

installPlanTimeout time.Duration
bundleUnpackTimeout time.Duration
Expand Down Expand Up @@ -77,6 +79,12 @@ func newRootCmd() *cobra.Command {
cmd.Flags().StringVar(&o.tlsKeyPath, "tls-key", "", "path to use for private key (requires tls-cert)")
cmd.Flags().StringVar(&o.tlsCertPath, "tls-cert", "", "path to use for certificate key (requires tls-key)")
cmd.Flags().StringVar(&o.clientCAPath, "client-ca", "", "path to watch for client ca bundle")
cmd.Flags().BoolVar(&o.clientCAAuthorization, "client-ca-authorization", false,
"authorize metrics scrapers by verifying their client certificate against the --client-ca bundle (mutual TLS) "+
"instead of using Kubernetes token/SubjectAccessReview authorization; requires --tls-cert, --tls-key, and --client-ca")
cmd.Flags().StringSliceVar(&o.clientCAAllowedCommonNames, "client-ca-allowed-cn", nil,
"comma-separated list of client certificate common names authorized to scrape metrics when --client-ca-authorization is set; "+
"if empty, any certificate that verifies against --client-ca is authorized")

cmd.Flags().DurationVar(&o.wakeupInterval, "interval", defaultWakeupInterval, "wakeup interval")
cmd.Flags().DurationVar(&o.bundleUnpackTimeout, "bundle-unpack-timeout", 10*time.Minute, "The time limit for bundle unpacking, after which InstallPlan execution is considered to have failed. 0 is considered as having no timeout.")
Expand Down
10 changes: 10 additions & 0 deletions cmd/olm/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,14 @@ var (

clientCAPath = pflag.String("client-ca", "", "path to watch for client ca bundle")

clientCAAuthorization = pflag.Bool("client-ca-authorization", false,
"authorize metrics scrapers by verifying their client certificate against the --client-ca bundle (mutual TLS) "+
"instead of using Kubernetes token/SubjectAccessReview authorization; requires --tls-cert, --tls-key, and --client-ca")

clientCAAllowedCommonNames = pflag.StringSlice("client-ca-allowed-cn", nil,
"comma-separated list of client certificate common names authorized to scrape metrics when --client-ca-authorization is set; "+
"if empty, any certificate that verifies against --client-ca is authorized")

namespace = pflag.String(
"namespace", "", "namespace where cleanup runs")
)
Expand Down Expand Up @@ -176,6 +184,8 @@ func main() {
listenAndServe, err := server.GetListenAndServeFunc(
server.WithLogger(logger),
server.WithTLS(tlsCertPath, tlsKeyPath, clientCAPath),
server.WithClientCAAuthorization(*clientCAAuthorization),
server.WithClientCAAllowedCommonNames(*clientCAAllowedCommonNames),
server.WithKubeConfig(config),
server.WithAPIServerTLSQuerier(apiServerTLSQuerier),
server.WithDebug(*debug),
Expand Down
26 changes: 18 additions & 8 deletions pkg/lib/filemonitor/cabundle_updater.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package filemonitor

import (
"crypto/x509"
"fmt"
"os"
"sync"

Expand All @@ -21,7 +22,9 @@ func NewCertPoolStore(clientCAPath string) (*certPoolStore, error) {
return nil, err
}
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(pem)
if !pool.AppendCertsFromPEM(pem) {
return nil, fmt.Errorf("client CA bundle %q contains no parseable certificates", clientCAPath)
}

return &certPoolStore{
mutex: sync.RWMutex{},
Expand All @@ -32,14 +35,19 @@ func NewCertPoolStore(clientCAPath string) (*certPoolStore, error) {

func (c *certPoolStore) storeCABundle(clientCAPath string) error {
pem, err := os.ReadFile(clientCAPath)
if err == nil {
c.mutex.Lock()
defer c.mutex.Unlock()
pool := x509.NewCertPool()
pool.AppendCertsFromPEM(pem)
c.certpool = pool
if err != nil {
return err
}
pool := x509.NewCertPool()
if !pool.AppendCertsFromPEM(pem) {
// Preserve the existing (usable) pool rather than replacing it with an
// empty one that can verify no client certificates.
return fmt.Errorf("client CA bundle %q contains no parseable certificates", clientCAPath)
}
return err
c.mutex.Lock()
defer c.mutex.Unlock()
c.certpool = pool
return nil
}

func (c *certPoolStore) HandleCABundleUpdate(logger logrus.FieldLogger, event fsnotify.Event) {
Expand All @@ -56,5 +64,7 @@ func (c *certPoolStore) HandleCABundleUpdate(logger logrus.FieldLogger, event fs
}

func (c *certPoolStore) GetCertPool() *x509.CertPool {
c.mutex.RLock()
defer c.mutex.RUnlock()
return c.certpool
}
105 changes: 105 additions & 0 deletions pkg/lib/filemonitor/cabundle_updater_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
package filemonitor

import (
"os"
"path/filepath"
"sync"
"testing"
"time"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestNewCertPoolStore_ValidBundle verifies a valid CA bundle loads successfully.
func TestNewCertPoolStore_ValidBundle(t *testing.T) {
store, err := NewCertPoolStore(filepath.Join("testdata", "ca.crt"))
require.NoError(t, err)
require.NotNil(t, store)
assert.NotNil(t, store.GetCertPool())
}

// TestNewCertPoolStore_RejectsBundleWithNoCerts verifies that a readable bundle
// that contains no parseable certificates is rejected, rather than silently
// producing an empty pool that can verify no client certificates.
func TestNewCertPoolStore_RejectsBundleWithNoCerts(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "ca.crt")
require.NoError(t, os.WriteFile(path, []byte("not a certificate\n"), 0644))

_, err := NewCertPoolStore(path)
assert.Error(t, err, "a bundle with no parseable certificates should be rejected")
}

// TestStoreCABundle_PreservesPoolOnInvalidUpdate verifies that an invalid CA
// bundle update does not replace a previously usable pool.
func TestStoreCABundle_PreservesPoolOnInvalidUpdate(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "ca.crt")

validPEM, err := os.ReadFile(filepath.Join("testdata", "ca.crt"))
require.NoError(t, err)
require.NoError(t, os.WriteFile(path, validPEM, 0644))

store, err := NewCertPoolStore(path)
require.NoError(t, err)
original := store.GetCertPool()
require.NotNil(t, original)

// Overwrite with an invalid bundle and attempt to reload.
require.NoError(t, os.WriteFile(path, []byte("garbage\n"), 0644))
err = store.storeCABundle(path)

assert.Error(t, err, "reloading an invalid bundle should return an error")
assert.Same(t, original, store.GetCertPool(), "the existing pool must be preserved on an invalid update")
}

// TestCertPoolStore_ConcurrentReadWrite exercises concurrent GetCertPool reads
// against storeCABundle writes. It is meaningful under the race detector
// (go test -race): GetCertPool must synchronize with the pool replacement.
func TestCertPoolStore_ConcurrentReadWrite(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "ca.crt")

validPEM, err := os.ReadFile(filepath.Join("testdata", "ca.crt"))
require.NoError(t, err)
require.NoError(t, os.WriteFile(path, validPEM, 0644))

store, err := NewCertPoolStore(path)
require.NoError(t, err)

stop := make(chan struct{})
var wg sync.WaitGroup

// Readers spin on GetCertPool.
for i := 0; i < 4; i++ {
wg.Add(1)
go func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
_ = store.GetCertPool()
}
}
}()
}

// Writer repeatedly replaces the pool.
wg.Add(1)
go func() {
defer wg.Done()
for i := 0; i < 1000; i++ {
if err := store.storeCABundle(path); err != nil {
t.Errorf("storeCABundle failed: %v", err)
return
}
}
}()

time.Sleep(50 * time.Millisecond)
close(stop)
wg.Wait()
}
Loading
Loading