Skip to content

Commit

Permalink
Allow k8s SA bearer token auth
Browse files Browse the repository at this point in the history
  • Loading branch information
bastjan committed Nov 9, 2023
1 parent 76600c9 commit d31bd1e
Show file tree
Hide file tree
Showing 5 changed files with 146 additions and 2 deletions.
2 changes: 1 addition & 1 deletion config/default/exporter_auth_proxy_patch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ spec:
cpu: 500m
memory: 128Mi
requests:
cpu: 5m
cpu: 10m
memory: 64Mi
- name: exporter
args:
Expand Down
2 changes: 1 addition & 1 deletion config/openshift4/exporter_openshift_patch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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/
Expand Down
93 changes: 93 additions & 0 deletions internal/saauth/saauth.go
Original file line number Diff line number Diff line change
@@ -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
}
40 changes: 40 additions & 0 deletions internal/saauth/saauth_test.go
Original file line number Diff line number Diff line change
@@ -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)
}
11 changes: 11 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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")
Expand All @@ -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")
Expand Down Expand Up @@ -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)

Expand Down

0 comments on commit d31bd1e

Please sign in to comment.