背景
k8s 1.32起新增了特性ExternalServiceAccountTokenSigner,用于从外部服务账号生成token和获取public key
在这之前都是在apiserver所在节点本地存储的,默认在/etc/kubernetes/pki/sa.pub和/etc/kubernetes/pki/sa.key
此特性在k8s 1.34默认开启,1.36后锁定为true
简单总结
apiserver 连接一个提供jwt 签名的grpc服务
当client调用serviceaccount token create api时,apiserver会调用此服务生成token
apiserver 会定期从此grpc服务获取public key更新到缓存,当client的serviceaccount token需要校验时候,从缓存获取public key进行验证
源码
参数相关
pkg/controlplane/apiserver/options/options.go中
构建serviceaccount配置
func (o *Options) completeServiceAccountOptions(ctx context.Context, completed *completedOptions) error {
...
构建serviceaccount插件和publickey cache
plugin, cache, err := plugin.New(ctx, completed.Authentication.ServiceAccountIssuers[0], completed.ServiceAccountSigningEndpoint, 60*time.Second, false)
...
设置serviceaccount插件和publickey cache
completed.ServiceAccountIssuer = plugin
completed.Authentication.ServiceAccounts.ExternalPublicKeysGetter = cache
...
}
生成相关
pkg/registry/core/serviceaccount/storage/token.go中
serviceaccount token api处理函数入口
func (r *TokenREST) Create(ctx context.Context, name string, obj runtime.Object, createValidation rest.ValidateObjectFunc, options *metav1.CreateOptions) (runtime.Object, error) {
...
构建serviceaccount token claims
sc, pc, err := token.Claims(*svcacct, pod, secret, node, exp, warnAfter, req.Spec.Audiences)
...
生成serviceaccount token
tokdata, err := r.issuer.GenerateToken(ctx, sc, pc)
...
}
pkg/serviceaccount/externaljwt/plugin/plugin.go中
构建serviceaccount token 插件和publickey cache
func New(ctx context.Context, issuer, socketPath string, keySyncTimeout time.Duration, allowSigningWithNonOIDCKeys bool) (*Plugin, *keyCache, error) {
...
链接serviceaccount token签名服务
conn, err := grpc.Dial(
socketPath,
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithAuthority("localhost"),
grpc.WithDefaultCallOptions(grpc.WaitForReady(true)),
grpc.WithContextDialer(func(ctx context.Context, path string) (net.Conn, error) {
return (&net.Dialer{}).DialContext(ctx, "unix", path)
}),
grpc.WithChainUnaryInterceptor(externaljwtmetrics.OuboundRequestMetricsInterceptor),
)
...
构建serviceaccount token 插件
plugin := newPlugin(issuer, conn, allowSigningWithNonOIDCKeys)
...
定期更新serviceaccount token publickey cache
go plugin.keyCache.scheduleSync(ctx, keySyncTimeout)
...
return plugin, plugin.keyCache, nil
}
构建serviceaccount token 插件
func newPlugin(iss string, conn *grpc.ClientConn, allowSigningWithNonOIDCKeys bool) *Plugin {
client := externaljwtv1.NewExternalJWTSignerClient(conn)
plugin := &Plugin{
iss: iss,
client: client,
allowSigningWithNonOIDCKeys: allowSigningWithNonOIDCKeys,
keyCache: newKeyCache(client),
}
return plugin
}
生成serviceaccount token
func (p *Plugin) GenerateToken(ctx context.Context, claims *jwt.Claims, privateClaims interface{}) (string, error) {
jwt, err := p.signAndAssembleJWT(ctx, claims, privateClaims)
...
}
生成serviceaccount token
func (p *Plugin) signAndAssembleJWT(ctx context.Context, claims *jwt.Claims, privateClaims interface{}) (string, error) {
...
生成serviceaccount token payload
payload, err := mergeClaims(p.iss, claims, privateClaims)
...
转换serviceaccount token payload为base64
payloadBase64 := base64.RawURLEncoding.EncodeToString(payload)
构建serviceaccount token 签名请求
request := &externaljwtv1.SignJWTRequest{
Claims: payloadBase64,
}
调用serviceaccount token 签名服务
response, err := p.client.Sign(ctx, request)
...
构建serviceaccount token
return response.Header + "." + payloadBase64 + "." + response.Signature, nil
}
校验相关
pkg/kubeapiserver/authenticator/config.go中
构建认证配置
func (config Config) New(serverLifecycle context.Context) (authenticator.Request, func(context.Context, *apiserver.AuthenticationConfiguration) error, *spec.SecurityDefinitions, spec3.SecuritySchemes, error) {
...
if len(config.ServiceAccountIssuers) > 0 && config.ServiceAccountPublicKeysGetter != nil {
serviceAccountAuth, err := newServiceAccountAuthenticator(config.ServiceAccountIssuers, config.ServiceAccountPublicKeysGetter, config.APIAudiences, config.ServiceAccountTokenGetter)
if err != nil {
return nil, nil, nil, nil, err
}
tokenAuthenticators = append(tokenAuthenticators, serviceAccountAuth)
}
...
}
构建serviceaccount token 认证器
func newServiceAccountAuthenticator(issuers []string, publicKeysGetter serviceaccount.PublicKeysGetter, apiAudiences authenticator.Audiences, serviceAccountGetter serviceaccount.ServiceAccountTokenGetter) (authenticator.Token, error) {
...
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator(issuers, publicKeysGetter, apiAudiences, serviceaccount.NewValidator(serviceAccountGetter))
return tokenAuthenticator, nil
}
pkg/serviceaccount/jwt.go中
构建serviceaccount token 认证器
func JWTTokenAuthenticator[PrivateClaims any](issuers []string, publicKeysGetter PublicKeysGetter, implicitAuds authenticator.Audiences, validator Validator[PrivateClaims]) authenticator.Token {
issuersMap := make(map[string]bool)
for _, issuer := range issuers {
issuersMap[issuer] = true
}
return &jwtTokenAuthenticator[PrivateClaims]{
issuers: issuersMap,
keysGetter: publicKeysGetter,
implicitAuds: implicitAuds,
validator: validator,
}
}
认证serviceaccount token
func (j *jwtTokenAuthenticator[PrivateClaims]) AuthenticateToken(ctx context.Context, tokenData string) (*authenticator.Response, bool, error) {
...
获取serviceaccount token publickey
keys := j.keysGetter.GetPublicKeys(ctx, kid)
...
校验
for _, key := range keys {
if err := tok.Claims(key.PublicKey, public, private); err != nil {
errlist = append(errlist, err)
continue
}
found = true
break
}
...
}