diff --git a/config/default/exporter_auth_proxy_patch.yaml b/config/default/exporter_auth_proxy_patch.yaml index f447534..7994618 100644 --- a/config/default/exporter_auth_proxy_patch.yaml +++ b/config/default/exporter_auth_proxy_patch.yaml @@ -29,7 +29,7 @@ spec: cpu: 500m memory: 128Mi requests: - cpu: 5m + cpu: 10m memory: 64Mi - name: exporter args: diff --git a/config/openshift4/exporter_openshift_patch.yaml b/config/openshift4/exporter_openshift_patch.yaml index 8d3d9db..b24d59c 100644 --- a/config/openshift4/exporter_openshift_patch.yaml +++ b/config/openshift4/exporter_openshift_patch.yaml @@ -14,7 +14,7 @@ spec: - --tls - --host=alertmanager-operated.openshift-monitoring.svc.cluster.local:9095 - --tls-server-name=alertmanager-main.openshift-monitoring.svc.cluster.local - - --bearer-token=$(cat /var/run/secrets/kubernetes.io/serviceaccount/token) + - --k8s-bearer-token-auth - --tls-ca-cert=/etc/ssl/certs/serving-certs/service-ca.crt volumeMounts: - mountPath: /etc/ssl/certs/serving-certs/ diff --git a/internal/saauth/saauth.go b/internal/saauth/saauth.go new file mode 100644 index 0000000..fab81f9 --- /dev/null +++ b/internal/saauth/saauth.go @@ -0,0 +1,93 @@ +package saauth + +import ( + "context" + "fmt" + "log" + "os" + "sync/atomic" + "time" + + "github.com/go-openapi/runtime" + "github.com/go-openapi/strfmt" +) + +// NewServiceAccountAuthInfoWriter creates a new ServiceAccountAuthInfoWriter. +// ServiceAccountAuthInfoWriter implements Kubernetes service account authentication. +// It reads the token from the given file and refreshes it every refreshInterval. +// If refreshInterval is 0, it defaults to 5 minutes. +// If saFile is empty, it defaults to /var/run/secrets/kubernetes.io/serviceaccount/token. +// An error is returned if the initial token read fails. Further read failures do not cause an error. +func NewServiceAccountAuthInfoWriter(saFile string, refreshInterval time.Duration) (*ServiceAccountAuthInfoWriter, error) { + if saFile == "" { + saFile = "/var/run/secrets/kubernetes.io/serviceaccount/token" + } + if refreshInterval == 0 { + refreshInterval = 5 * time.Minute + } + + w := &ServiceAccountAuthInfoWriter{ + ticker: time.NewTicker(refreshInterval), + saFile: saFile, + } + + t, err := w.readTokenFromFile() + if err != nil { + return nil, fmt.Errorf("failed to read token from file: %w", err) + } + w.storeToken(t) + + ctx, cancel := context.WithCancel(context.Background()) + w.cancel = cancel + + go func() { + for { + select { + case <-ctx.Done(): + return + case <-w.ticker.C: + t, err := w.readTokenFromFile() + if err != nil { + log.Printf("failed to read token from file: %v", err) + continue + } + w.storeToken(t) + } + } + }() + + return w, nil +} + +// ServiceAccountAuthInfoWriter implements Kubernetes service account authentication. +type ServiceAccountAuthInfoWriter struct { + saFile string + token atomic.Value + ticker *time.Ticker + cancel context.CancelFunc +} + +// AuthenticateRequest implements the runtime.ClientAuthInfoWriter interface. +// It sets the Authorization header to the current token. +func (s *ServiceAccountAuthInfoWriter) AuthenticateRequest(r runtime.ClientRequest, _ strfmt.Registry) error { + return r.SetHeaderParam(runtime.HeaderAuthorization, "Bearer "+s.loadToken()) +} + +// Stop stops the token refresh +func (s *ServiceAccountAuthInfoWriter) Stop() { + s.cancel() + s.ticker.Stop() +} + +func (s *ServiceAccountAuthInfoWriter) storeToken(t string) { + s.token.Store(t) +} + +func (s *ServiceAccountAuthInfoWriter) loadToken() string { + return s.token.Load().(string) +} + +func (s *ServiceAccountAuthInfoWriter) readTokenFromFile() (string, error) { + t, err := os.ReadFile(s.saFile) + return string(t), err +} diff --git a/internal/saauth/saauth_test.go b/internal/saauth/saauth_test.go new file mode 100644 index 0000000..b3fb9ad --- /dev/null +++ b/internal/saauth/saauth_test.go @@ -0,0 +1,40 @@ +package saauth_test + +import ( + "os" + "testing" + "time" + + "github.com/appuio/alerts_exporter/internal/saauth" + "github.com/go-openapi/runtime" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func Test_ServiceAccountAuthInfoWriter_AuthenticateRequest(t *testing.T) { + tokenFile := t.TempDir() + "/token" + + require.NoError(t, os.WriteFile(tokenFile, []byte("token"), 0644)) + + subject, err := saauth.NewServiceAccountAuthInfoWriter(tokenFile, time.Millisecond) + require.NoError(t, err) + defer subject.Stop() + + r := new(runtime.TestClientRequest) + require.NoError(t, subject.AuthenticateRequest(r, nil)) + require.Equal(t, "Bearer token", r.GetHeaderParams().Get("Authorization")) + + require.NoError(t, os.WriteFile(tokenFile, []byte("new-token"), 0644)) + require.EventuallyWithT(t, func(t *assert.CollectT) { + r := new(runtime.TestClientRequest) + require.NoError(t, subject.AuthenticateRequest(r, nil)) + require.Equal(t, "Bearer new-token", r.GetHeaderParams().Get("Authorization")) + }, time.Second, time.Millisecond) +} + +func Test_NewServiceAccountAuthInfoWriter_TokenReadErr(t *testing.T) { + tokenFile := t.TempDir() + "/token" + + _, err := saauth.NewServiceAccountAuthInfoWriter(tokenFile, time.Millisecond) + require.ErrorIs(t, err, os.ErrNotExist) +} diff --git a/main.go b/main.go index ccbb3f2..eb3891a 100644 --- a/main.go +++ b/main.go @@ -8,6 +8,7 @@ import ( "net/http" alertscollector "github.com/appuio/alerts_exporter/internal/alerts_collector" + "github.com/appuio/alerts_exporter/internal/saauth" openapiclient "github.com/go-openapi/runtime/client" alertmanagerclient "github.com/prometheus/alertmanager/api/v2/client" "github.com/prometheus/client_golang/prometheus" @@ -24,6 +25,7 @@ var tlsCert, tlsCertKey, tlsCaCert, tlsServerName string var tlsInsecure bool var useTLS bool var bearerToken string +var k8sBearerTokenAuth bool func main() { flag.StringVar(&listenAddr, "listen-addr", ":8080", "The addr to listen on") @@ -38,6 +40,7 @@ func main() { flag.BoolVar(&tlsInsecure, "insecure", false, "Disable TLS host verification") flag.StringVar(&bearerToken, "bearer-token", "", "Bearer token to use for authentication") + flag.BoolVar(&k8sBearerTokenAuth, "k8s-bearer-token-auth", false, "Use Kubernetes service account bearer token for authentication") flag.BoolVar(&withActive, "with-active", true, "Query for active alerts") flag.BoolVar(&withInhibited, "with-inhibited", true, "Query for inhibited alerts") @@ -72,6 +75,14 @@ func main() { if bearerToken != "" { rt.DefaultAuthentication = openapiclient.BearerToken(bearerToken) } + if k8sBearerTokenAuth { + sa, err := saauth.NewServiceAccountAuthInfoWriter("", 0) + if err != nil { + log.Fatal(err) + } + defer sa.Stop() + rt.DefaultAuthentication = sa + } ac := alertmanagerclient.New(rt, nil)