K8s: standalone authenticator that allows a type of downstream forwarding (#85130)

This commit is contained in:
Charandas 2024-03-28 11:52:28 -07:00 committed by GitHub
parent 3c28a3d494
commit 0f1151964c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
10 changed files with 198 additions and 3 deletions

1
.vscode/launch.json vendored
View File

@ -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"]
},

2
go.mod
View File

@ -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

2
go.sum
View File

@ -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=

View File

@ -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
```

View File

@ -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
}
}

View File

@ -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
}

View File

@ -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
}

View File

@ -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")

View File

@ -0,0 +1,9 @@
package options
import "github.com/grafana/authlib/authn"
func NewAuthnOptions() *AuthnOptions {
return &AuthnOptions{
IDVerifierConfig: &authn.IDVerifierConfig{},
}
}

View File

@ -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
}