Skip to content

Commit 0f11519

Browse files
authored
K8s: standalone authenticator that allows a type of downstream forwarding (#85130)
1 parent 3c28a3d commit 0f11519

File tree

10 files changed

+198
-3
lines changed

10 files changed

+198
-3
lines changed

.vscode/launch.json

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"args": ["apiserver",
3535
"--secure-port=8443",
3636
"--runtime-config=query.grafana.app/v0alpha1=true",
37+
"--grafana.authn.signing-keys-url=http://localhost:3000/api/signing-keys/keys",
3738
"--hg-url=http://localhost:3000",
3839
"--hg-key=$HGAPIKEY"]
3940
},

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -474,6 +474,8 @@ require github.com/jackc/pgx/v5 v5.5.5 // @grafana/oss-big-tent
474474

475475
require github.com/getkin/kin-openapi v0.120.0 // @grafana/grafana-as-code
476476

477+
require github.com/grafana/authlib v0.0.0-20240319083410-9d4a6e3861e5 // @grafana/grafana-app-platform-squad
478+
477479
require (
478480
github.com/bahlo/generic-list-go v0.2.0 // indirect
479481
github.com/buger/jsonparser v1.1.1 // indirect

go.sum

+2
Original file line numberDiff line numberDiff line change
@@ -2161,6 +2161,8 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
21612161
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
21622162
github.com/grafana/alerting v0.0.0-20240322221449-89ae4e299bf8 h1:ndBSFAHmJRWqln2uNys7lV0+9U8tlW6ZuNz8ETW60Us=
21632163
github.com/grafana/alerting v0.0.0-20240322221449-89ae4e299bf8/go.mod h1:0nHKO0w8OTemvZ3eh7+s1EqGGhgbs0kvkTeLU1FrbTw=
2164+
github.com/grafana/authlib v0.0.0-20240319083410-9d4a6e3861e5 h1:A13Z8Hy60BfIduM819kpk0njrRKjbAVbVRhE+R+AF/8=
2165+
github.com/grafana/authlib v0.0.0-20240319083410-9d4a6e3861e5/go.mod h1:86rRD5P6u2JPWtNWTMOlqlU+YMv2fUvVz/DomA6L7w4=
21642166
github.com/grafana/codejen v0.0.3 h1:tAWxoTUuhgmEqxJPOLtJoxlPBbMULFwKFOcRsPRPXDw=
21652167
github.com/grafana/codejen v0.0.3/go.mod h1:zmwwM/DRyQB7pfuBjTWII3CWtxcXh8LTwAYGfDfpR6s=
21662168
github.com/grafana/cue v0.0.0-20230926092038-971951014e3f h1:TmYAMnqg3d5KYEAaT6PtTguL2GjLfvr6wnAX8Azw6tQ=

pkg/cmd/grafana/apiserver/apiserver.md

+4
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,14 @@ aggregation path altogether and just run this example apiserver as a standalone
1111

1212
### Usage
1313

14+
For setting `--grafana.authn.signing-keys-url`, Grafana must be run with `idForwarding = true` while also ensuring
15+
you have logged in to the instance at least once.
16+
1417
```shell
1518
go run ./pkg/cmd/grafana apiserver \
1619
--runtime-config=example.grafana.app/v0alpha1=true \
1720
--grafana-apiserver-dev-mode \
21+
--grafana.authn.signing-keys-url="http://localhost:3000/api/signing-keys/keys" \
1822
--verbosity 10 \
1923
--secure-port 7443
2024
```
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package auth
2+
3+
import (
4+
"net/http"
5+
"strings"
6+
7+
"github.com/grafana/authlib/authn"
8+
"k8s.io/apiserver/pkg/authentication/authenticator"
9+
"k8s.io/apiserver/pkg/authentication/user"
10+
)
11+
12+
const (
13+
headerKeyAccessToken = "X-Access-Token"
14+
headerKeyGrafanaID = "X-Grafana-Id"
15+
16+
extraKeyAccessToken = "access-token"
17+
extraKeyGrafanaID = "id-token"
18+
extraKeyGLSA = "glsa"
19+
)
20+
21+
func NewAccessTokenAuthenticator(config *authn.IDVerifierConfig) authenticator.RequestFunc {
22+
verifier := authn.NewVerifier[CustomClaims](authn.IDVerifierConfig{
23+
SigningKeysURL: config.SigningKeysURL,
24+
AllowedAudiences: config.AllowedAudiences,
25+
})
26+
return getAccessTokenAuthenticatorFunc(&TokenValidator{verifier})
27+
}
28+
29+
func getAccessTokenAuthenticatorFunc(validator *TokenValidator) authenticator.RequestFunc {
30+
return func(req *http.Request) (*authenticator.Response, bool, error) {
31+
accessToken := req.Header.Get(headerKeyAccessToken)
32+
if accessToken == "" {
33+
return nil, false, nil
34+
}
35+
36+
// While the authn token system is in development, we can temporarily use
37+
// service account tokens. Note this does not grant any real permissions/verification,
38+
// it simply allows forwarding the token to the next request
39+
if strings.HasPrefix(accessToken, "glsa_") {
40+
return &authenticator.Response{
41+
Audiences: authenticator.Audiences([]string{}),
42+
User: &user.DefaultInfo{
43+
Name: "glsa-forwarding-request",
44+
UID: "",
45+
Groups: []string{},
46+
Extra: map[string][]string{
47+
extraKeyGLSA: {accessToken},
48+
},
49+
},
50+
}, true, nil
51+
}
52+
53+
result, err := validator.Validate(req.Context(), accessToken)
54+
if err != nil {
55+
return nil, false, err
56+
}
57+
58+
return &authenticator.Response{
59+
Audiences: authenticator.Audiences(result.Claims.Audience),
60+
User: &user.DefaultInfo{
61+
Name: result.Subject,
62+
UID: "",
63+
Groups: []string{},
64+
Extra: map[string][]string{
65+
extraKeyAccessToken: {accessToken},
66+
extraKeyGrafanaID: {req.Header.Get("X-Grafana-Id")}, // this may exist if starting with a user
67+
},
68+
},
69+
}, true, nil
70+
}
71+
}
+57
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
"fmt"
6+
7+
"k8s.io/apiserver/pkg/authentication/authenticator"
8+
"k8s.io/apiserver/pkg/authentication/request/union"
9+
"k8s.io/apiserver/pkg/endpoints/request"
10+
)
11+
12+
func AppendToAuthenticators(newAuthenticator authenticator.RequestFunc, authRequestHandlers ...authenticator.Request) authenticator.Request {
13+
handlers := append([]authenticator.Request{newAuthenticator}, authRequestHandlers...)
14+
return union.New(handlers...)
15+
}
16+
17+
// Get tokens that can be forwarded to the next service
18+
// In the future this will need to create new tokens with a new audience
19+
func GetIDForwardingAuthHeaders(ctx context.Context) (map[string]string, error) {
20+
user, ok := request.UserFrom(ctx)
21+
if !ok {
22+
return nil, fmt.Errorf("missing user")
23+
}
24+
25+
getter := func(key string) string {
26+
vals, ok := user.GetExtra()[key]
27+
if ok && len(vals) == 1 {
28+
return vals[0]
29+
}
30+
return ""
31+
}
32+
33+
token := getter(extraKeyGLSA)
34+
if token != "" {
35+
// Service account tokens get forwarded as auth tokens
36+
// this lets us keep testing the workflows while the ID token system is in dev
37+
return map[string]string{
38+
"Authorization": "Bearer " + token,
39+
}, nil
40+
}
41+
42+
accessToken := getter(extraKeyAccessToken)
43+
if accessToken == "" {
44+
return nil, fmt.Errorf("missing access token in user info")
45+
}
46+
47+
idToken := getter(extraKeyGrafanaID)
48+
if idToken != "" {
49+
return map[string]string{
50+
headerKeyAccessToken: accessToken,
51+
headerKeyGrafanaID: idToken,
52+
}, nil
53+
}
54+
return map[string]string{
55+
headerKeyAccessToken: accessToken,
56+
}, nil
57+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
package auth
2+
3+
import (
4+
"context"
5+
6+
"github.com/grafana/authlib/authn"
7+
)
8+
9+
type CustomClaims struct {
10+
// Nothing yet
11+
}
12+
13+
type TokenValidator struct {
14+
verifier authn.Verifier[CustomClaims]
15+
}
16+
17+
func (v *TokenValidator) Validate(ctx context.Context, token string) (*authn.Claims[CustomClaims], error) {
18+
customClaims, err := v.verifier.Verify(ctx, token)
19+
if err != nil {
20+
return nil, err
21+
}
22+
23+
return customClaims, nil
24+
}

pkg/cmd/grafana/apiserver/server.go

+11-3
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,26 @@ import (
66
"net"
77
"path"
88

9+
"github.com/spf13/pflag"
910
"k8s.io/apimachinery/pkg/runtime/schema"
1011
utilerrors "k8s.io/apimachinery/pkg/util/errors"
1112
genericapiserver "k8s.io/apiserver/pkg/server"
1213
"k8s.io/client-go/tools/clientcmd"
1314
netutils "k8s.io/utils/net"
1415

1516
"github.com/grafana/grafana/pkg/apiserver/builder"
17+
"github.com/grafana/grafana/pkg/cmd/grafana/apiserver/auth"
1618
"github.com/grafana/grafana/pkg/infra/log"
1719
"github.com/grafana/grafana/pkg/infra/tracing"
1820
grafanaAPIServer "github.com/grafana/grafana/pkg/services/apiserver"
1921
"github.com/grafana/grafana/pkg/services/apiserver/standalone"
2022
standaloneoptions "github.com/grafana/grafana/pkg/services/apiserver/standalone/options"
2123
"github.com/grafana/grafana/pkg/services/apiserver/utils"
2224
"github.com/grafana/grafana/pkg/setting"
23-
"github.com/spf13/pflag"
2425
)
2526

2627
const (
27-
defaultEtcdPathPrefix = "/registry/grafana.app"
28-
dataPath = "data/grafana-apiserver" // same as grafana core
28+
dataPath = "data/grafana-apiserver" // same as grafana core
2929
)
3030

3131
// APIServerOptions contains the state for the apiserver
@@ -101,6 +101,14 @@ func (o *APIServerOptions) Config() (*genericapiserver.RecommendedConfig, error)
101101
return nil, fmt.Errorf("failed to apply options to server config: %w", err)
102102
}
103103

104+
// When the ID signing key exists, configure access-token support
105+
if len(o.Options.AuthnOptions.IDVerifierConfig.SigningKeysURL) > 0 {
106+
serverConfig.Authentication.Authenticator = auth.AppendToAuthenticators(
107+
auth.NewAccessTokenAuthenticator(o.Options.AuthnOptions.IDVerifierConfig),
108+
serverConfig.Authentication.Authenticator,
109+
)
110+
}
111+
104112
serverConfig.DisabledPostStartHooks = serverConfig.DisabledPostStartHooks.Insert("generic-apiserver-start-informers")
105113
serverConfig.DisabledPostStartHooks = serverConfig.DisabledPostStartHooks.Insert("priority-and-fairness-config-consumer")
106114

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
package options
2+
3+
import "github.com/grafana/authlib/authn"
4+
5+
func NewAuthnOptions() *AuthnOptions {
6+
return &AuthnOptions{
7+
IDVerifierConfig: &authn.IDVerifierConfig{},
8+
}
9+
}

pkg/services/apiserver/standalone/options/options.go

+17
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ import (
77
"k8s.io/apimachinery/pkg/runtime"
88
genericapiserver "k8s.io/apiserver/pkg/server"
99
genericoptions "k8s.io/apiserver/pkg/server/options"
10+
11+
"github.com/grafana/authlib/authn"
1012
)
1113

1214
type Options struct {
@@ -15,6 +17,7 @@ type Options struct {
1517
RecommendedOptions *genericoptions.RecommendedOptions
1618
TracingOptions *TracingOptions
1719
MetricsOptions *MetricsOptions
20+
AuthnOptions *AuthnOptions
1821
}
1922

2023
func New(logger log.Logger, codec runtime.Codec) *Options {
@@ -24,6 +27,7 @@ func New(logger log.Logger, codec runtime.Codec) *Options {
2427
RecommendedOptions: options.NewRecommendedOptions(codec),
2528
TracingOptions: NewTracingOptions(logger),
2629
MetricsOptions: NewMetrcicsOptions(logger),
30+
AuthnOptions: NewAuthnOptions(),
2731
}
2832
}
2933

@@ -33,6 +37,7 @@ func (o *Options) AddFlags(fs *pflag.FlagSet) {
3337
o.RecommendedOptions.AddFlags(fs)
3438
o.TracingOptions.AddFlags(fs)
3539
o.MetricsOptions.AddFlags(fs)
40+
o.AuthnOptions.AddFlags(fs)
3641
}
3742

3843
func (o *Options) Validate() []error {
@@ -157,3 +162,15 @@ func (o *Options) ApplyTo(serverConfig *genericapiserver.RecommendedConfig) erro
157162

158163
return nil
159164
}
165+
166+
type AuthnOptions struct {
167+
IDVerifierConfig *authn.IDVerifierConfig
168+
}
169+
170+
func (authOpts *AuthnOptions) AddFlags(fs *pflag.FlagSet) {
171+
prefix := "grafana.authn"
172+
fs.StringVar(&authOpts.IDVerifierConfig.SigningKeysURL, prefix+".signing-keys-url", "", "URL to jwks endpoint")
173+
174+
audience := fs.StringSlice(prefix+".allowed-audiences", []string{}, "Specifies a comma-separated list of allowed audiences.")
175+
authOpts.IDVerifierConfig.AllowedAudiences = *audience
176+
}

0 commit comments

Comments
 (0)