From 0f1151964cb9ca0a77093292242d84aa57c4e754 Mon Sep 17 00:00:00 2001 From: Charandas Date: Thu, 28 Mar 2024 11:52:28 -0700 Subject: [PATCH] K8s: standalone authenticator that allows a type of downstream forwarding (#85130) --- .vscode/launch.json | 1 + go.mod | 2 + go.sum | 2 + pkg/cmd/grafana/apiserver/apiserver.md | 4 ++ .../apiserver/auth/tokenAuthenticator.go | 71 +++++++++++++++++++ pkg/cmd/grafana/apiserver/auth/util.go | 57 +++++++++++++++ pkg/cmd/grafana/apiserver/auth/validator.go | 24 +++++++ pkg/cmd/grafana/apiserver/server.go | 14 +++- .../apiserver/standalone/options/authn.go | 9 +++ .../apiserver/standalone/options/options.go | 17 +++++ 10 files changed, 198 insertions(+), 3 deletions(-) create mode 100644 pkg/cmd/grafana/apiserver/auth/tokenAuthenticator.go create mode 100644 pkg/cmd/grafana/apiserver/auth/util.go create mode 100644 pkg/cmd/grafana/apiserver/auth/validator.go create mode 100644 pkg/services/apiserver/standalone/options/authn.go diff --git a/.vscode/launch.json b/.vscode/launch.json index c4cbc37dadc..99a3f849ca9 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -34,6 +34,7 @@ "args": ["apiserver", "--secure-port=8443", "--runtime-config=query.grafana.app/v0alpha1=true", + "--grafana.authn.signing-keys-url=http://localhost:3000/api/signing-keys/keys", "--hg-url=http://localhost:3000", "--hg-key=$HGAPIKEY"] }, diff --git a/go.mod b/go.mod index e0c5fa5c44f..e4514c19ec6 100644 --- a/go.mod +++ b/go.mod @@ -474,6 +474,8 @@ require github.com/jackc/pgx/v5 v5.5.5 // @grafana/oss-big-tent require github.com/getkin/kin-openapi v0.120.0 // @grafana/grafana-as-code +require github.com/grafana/authlib v0.0.0-20240319083410-9d4a6e3861e5 // @grafana/grafana-app-platform-squad + require ( github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect diff --git a/go.sum b/go.sum index 13788a4902b..52df46fc8c8 100644 --- a/go.sum +++ b/go.sum @@ -2161,6 +2161,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grafana/alerting v0.0.0-20240322221449-89ae4e299bf8 h1:ndBSFAHmJRWqln2uNys7lV0+9U8tlW6ZuNz8ETW60Us= github.com/grafana/alerting v0.0.0-20240322221449-89ae4e299bf8/go.mod h1:0nHKO0w8OTemvZ3eh7+s1EqGGhgbs0kvkTeLU1FrbTw= +github.com/grafana/authlib v0.0.0-20240319083410-9d4a6e3861e5 h1:A13Z8Hy60BfIduM819kpk0njrRKjbAVbVRhE+R+AF/8= +github.com/grafana/authlib v0.0.0-20240319083410-9d4a6e3861e5/go.mod h1:86rRD5P6u2JPWtNWTMOlqlU+YMv2fUvVz/DomA6L7w4= github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw= github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s= github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ= diff --git a/pkg/cmd/grafana/apiserver/apiserver.md b/pkg/cmd/grafana/apiserver/apiserver.md index b84fa70dfe2..41fe0362570 100644 --- a/pkg/cmd/grafana/apiserver/apiserver.md +++ b/pkg/cmd/grafana/apiserver/apiserver.md @@ -11,10 +11,14 @@ aggregation path altogether and just run this example apiserver as a standalone ### Usage +For setting `--grafana.authn.signing-keys-url`, Grafana must be run with `idForwarding = true` while also ensuring +you have logged in to the instance at least once. + ```shell go run ./pkg/cmd/grafana apiserver \ --runtime-config=example.grafana.app/v0alpha1=true \ --grafana-apiserver-dev-mode \ + --grafana.authn.signing-keys-url="http://localhost:3000/api/signing-keys/keys" \ --verbosity 10 \ --secure-port 7443 ``` diff --git a/pkg/cmd/grafana/apiserver/auth/tokenAuthenticator.go b/pkg/cmd/grafana/apiserver/auth/tokenAuthenticator.go new file mode 100644 index 00000000000..a4ad277ab60 --- /dev/null +++ b/pkg/cmd/grafana/apiserver/auth/tokenAuthenticator.go @@ -0,0 +1,71 @@ +package auth + +import ( + "net/http" + "strings" + + "github.com/grafana/authlib/authn" + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/user" +) + +const ( + headerKeyAccessToken = "X-Access-Token" + headerKeyGrafanaID = "X-Grafana-Id" + + extraKeyAccessToken = "access-token" + extraKeyGrafanaID = "id-token" + extraKeyGLSA = "glsa" +) + +func NewAccessTokenAuthenticator(config *authn.IDVerifierConfig) authenticator.RequestFunc { + verifier := authn.NewVerifier[CustomClaims](authn.IDVerifierConfig{ + SigningKeysURL: config.SigningKeysURL, + AllowedAudiences: config.AllowedAudiences, + }) + return getAccessTokenAuthenticatorFunc(&TokenValidator{verifier}) +} + +func getAccessTokenAuthenticatorFunc(validator *TokenValidator) authenticator.RequestFunc { + return func(req *http.Request) (*authenticator.Response, bool, error) { + accessToken := req.Header.Get(headerKeyAccessToken) + if accessToken == "" { + return nil, false, nil + } + + // While the authn token system is in development, we can temporarily use + // service account tokens. Note this does not grant any real permissions/verification, + // it simply allows forwarding the token to the next request + if strings.HasPrefix(accessToken, "glsa_") { + return &authenticator.Response{ + Audiences: authenticator.Audiences([]string{}), + User: &user.DefaultInfo{ + Name: "glsa-forwarding-request", + UID: "", + Groups: []string{}, + Extra: map[string][]string{ + extraKeyGLSA: {accessToken}, + }, + }, + }, true, nil + } + + result, err := validator.Validate(req.Context(), accessToken) + if err != nil { + return nil, false, err + } + + return &authenticator.Response{ + Audiences: authenticator.Audiences(result.Claims.Audience), + User: &user.DefaultInfo{ + Name: result.Subject, + UID: "", + Groups: []string{}, + Extra: map[string][]string{ + extraKeyAccessToken: {accessToken}, + extraKeyGrafanaID: {req.Header.Get("X-Grafana-Id")}, // this may exist if starting with a user + }, + }, + }, true, nil + } +} diff --git a/pkg/cmd/grafana/apiserver/auth/util.go b/pkg/cmd/grafana/apiserver/auth/util.go new file mode 100644 index 00000000000..a6c791c807a --- /dev/null +++ b/pkg/cmd/grafana/apiserver/auth/util.go @@ -0,0 +1,57 @@ +package auth + +import ( + "context" + "fmt" + + "k8s.io/apiserver/pkg/authentication/authenticator" + "k8s.io/apiserver/pkg/authentication/request/union" + "k8s.io/apiserver/pkg/endpoints/request" +) + +func AppendToAuthenticators(newAuthenticator authenticator.RequestFunc, authRequestHandlers ...authenticator.Request) authenticator.Request { + handlers := append([]authenticator.Request{newAuthenticator}, authRequestHandlers...) + return union.New(handlers...) +} + +// Get tokens that can be forwarded to the next service +// In the future this will need to create new tokens with a new audience +func GetIDForwardingAuthHeaders(ctx context.Context) (map[string]string, error) { + user, ok := request.UserFrom(ctx) + if !ok { + return nil, fmt.Errorf("missing user") + } + + getter := func(key string) string { + vals, ok := user.GetExtra()[key] + if ok && len(vals) == 1 { + return vals[0] + } + return "" + } + + token := getter(extraKeyGLSA) + if token != "" { + // Service account tokens get forwarded as auth tokens + // this lets us keep testing the workflows while the ID token system is in dev + return map[string]string{ + "Authorization": "Bearer " + token, + }, nil + } + + accessToken := getter(extraKeyAccessToken) + if accessToken == "" { + return nil, fmt.Errorf("missing access token in user info") + } + + idToken := getter(extraKeyGrafanaID) + if idToken != "" { + return map[string]string{ + headerKeyAccessToken: accessToken, + headerKeyGrafanaID: idToken, + }, nil + } + return map[string]string{ + headerKeyAccessToken: accessToken, + }, nil +} diff --git a/pkg/cmd/grafana/apiserver/auth/validator.go b/pkg/cmd/grafana/apiserver/auth/validator.go new file mode 100644 index 00000000000..394874b01c0 --- /dev/null +++ b/pkg/cmd/grafana/apiserver/auth/validator.go @@ -0,0 +1,24 @@ +package auth + +import ( + "context" + + "github.com/grafana/authlib/authn" +) + +type CustomClaims struct { + // Nothing yet +} + +type TokenValidator struct { + verifier authn.Verifier[CustomClaims] +} + +func (v *TokenValidator) Validate(ctx context.Context, token string) (*authn.Claims[CustomClaims], error) { + customClaims, err := v.verifier.Verify(ctx, token) + if err != nil { + return nil, err + } + + return customClaims, nil +} diff --git a/pkg/cmd/grafana/apiserver/server.go b/pkg/cmd/grafana/apiserver/server.go index 18407831d7a..4c0251a141c 100644 --- a/pkg/cmd/grafana/apiserver/server.go +++ b/pkg/cmd/grafana/apiserver/server.go @@ -6,6 +6,7 @@ import ( "net" "path" + "github.com/spf13/pflag" "k8s.io/apimachinery/pkg/runtime/schema" utilerrors "k8s.io/apimachinery/pkg/util/errors" genericapiserver "k8s.io/apiserver/pkg/server" @@ -13,6 +14,7 @@ import ( netutils "k8s.io/utils/net" "github.com/grafana/grafana/pkg/apiserver/builder" + "github.com/grafana/grafana/pkg/cmd/grafana/apiserver/auth" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/tracing" grafanaAPIServer "github.com/grafana/grafana/pkg/services/apiserver" @@ -20,12 +22,10 @@ import ( standaloneoptions "github.com/grafana/grafana/pkg/services/apiserver/standalone/options" "github.com/grafana/grafana/pkg/services/apiserver/utils" "github.com/grafana/grafana/pkg/setting" - "github.com/spf13/pflag" ) const ( - defaultEtcdPathPrefix = "/registry/grafana.app" - dataPath = "data/grafana-apiserver" // same as grafana core + dataPath = "data/grafana-apiserver" // same as grafana core ) // APIServerOptions contains the state for the apiserver @@ -101,6 +101,14 @@ func (o *APIServerOptions) Config() (*genericapiserver.RecommendedConfig, error) return nil, fmt.Errorf("failed to apply options to server config: %w", err) } + // When the ID signing key exists, configure access-token support + if len(o.Options.AuthnOptions.IDVerifierConfig.SigningKeysURL) > 0 { + serverConfig.Authentication.Authenticator = auth.AppendToAuthenticators( + auth.NewAccessTokenAuthenticator(o.Options.AuthnOptions.IDVerifierConfig), + serverConfig.Authentication.Authenticator, + ) + } + serverConfig.DisabledPostStartHooks = serverConfig.DisabledPostStartHooks.Insert("generic-apiserver-start-informers") serverConfig.DisabledPostStartHooks = serverConfig.DisabledPostStartHooks.Insert("priority-and-fairness-config-consumer") diff --git a/pkg/services/apiserver/standalone/options/authn.go b/pkg/services/apiserver/standalone/options/authn.go new file mode 100644 index 00000000000..81dd44cc98a --- /dev/null +++ b/pkg/services/apiserver/standalone/options/authn.go @@ -0,0 +1,9 @@ +package options + +import "github.com/grafana/authlib/authn" + +func NewAuthnOptions() *AuthnOptions { + return &AuthnOptions{ + IDVerifierConfig: &authn.IDVerifierConfig{}, + } +} diff --git a/pkg/services/apiserver/standalone/options/options.go b/pkg/services/apiserver/standalone/options/options.go index 696b4fe4956..173db33a48c 100644 --- a/pkg/services/apiserver/standalone/options/options.go +++ b/pkg/services/apiserver/standalone/options/options.go @@ -7,6 +7,8 @@ import ( "k8s.io/apimachinery/pkg/runtime" genericapiserver "k8s.io/apiserver/pkg/server" genericoptions "k8s.io/apiserver/pkg/server/options" + + "github.com/grafana/authlib/authn" ) type Options struct { @@ -15,6 +17,7 @@ type Options struct { RecommendedOptions *genericoptions.RecommendedOptions TracingOptions *TracingOptions MetricsOptions *MetricsOptions + AuthnOptions *AuthnOptions } func New(logger log.Logger, codec runtime.Codec) *Options { @@ -24,6 +27,7 @@ func New(logger log.Logger, codec runtime.Codec) *Options { RecommendedOptions: options.NewRecommendedOptions(codec), TracingOptions: NewTracingOptions(logger), MetricsOptions: NewMetrcicsOptions(logger), + AuthnOptions: NewAuthnOptions(), } } @@ -33,6 +37,7 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) { o.RecommendedOptions.AddFlags(fs) o.TracingOptions.AddFlags(fs) o.MetricsOptions.AddFlags(fs) + o.AuthnOptions.AddFlags(fs) } func (o *Options) Validate() []error { @@ -157,3 +162,15 @@ func (o *Options) ApplyTo(serverConfig *genericapiserver.RecommendedConfig) erro return nil } + +type AuthnOptions struct { + IDVerifierConfig *authn.IDVerifierConfig +} + +func (authOpts *AuthnOptions) AddFlags(fs *pflag.FlagSet) { + prefix := "grafana.authn" + fs.StringVar(&authOpts.IDVerifierConfig.SigningKeysURL, prefix+".signing-keys-url", "", "URL to jwks endpoint") + + audience := fs.StringSlice(prefix+".allowed-audiences", []string{}, "Specifies a comma-separated list of allowed audiences.") + authOpts.IDVerifierConfig.AllowedAudiences = *audience +}