blob: 2528802443c9624d2fcca3f03270d7b079baaf06 [file] [log] [blame]
// Copyright Istio Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package kubeauth
import (
"fmt"
"net/http"
"strings"
)
import (
"golang.org/x/net/context"
"google.golang.org/grpc/metadata"
"istio.io/pkg/log"
"k8s.io/client-go/kubernetes"
)
import (
"github.com/apache/dubbo-go-pixiu/pkg/cluster"
"github.com/apache/dubbo-go-pixiu/pkg/config/mesh"
"github.com/apache/dubbo-go-pixiu/pkg/jwt"
"github.com/apache/dubbo-go-pixiu/pkg/security"
"github.com/apache/dubbo-go-pixiu/security/pkg/k8s/tokenreview"
"github.com/apache/dubbo-go-pixiu/security/pkg/server/ca/authenticate"
"github.com/apache/dubbo-go-pixiu/security/pkg/util"
)
const (
KubeJWTAuthenticatorType = "KubeJWTAuthenticator"
clusterIDMeta = "clusterid"
)
type RemoteKubeClientGetter func(clusterID cluster.ID) kubernetes.Interface
// KubeJWTAuthenticator authenticates K8s JWTs.
type KubeJWTAuthenticator struct {
// holder of a mesh configuration for dynamically updating trust domain
meshHolder mesh.Holder
jwtPolicy string
// Primary cluster kube client
kubeClient kubernetes.Interface
// Primary cluster ID
clusterID cluster.ID
// remote cluster kubeClient getter
remoteKubeClientGetter RemoteKubeClientGetter
}
var _ security.Authenticator = &KubeJWTAuthenticator{}
// NewKubeJWTAuthenticator creates a new kubeJWTAuthenticator.
func NewKubeJWTAuthenticator(meshHolder mesh.Holder, client kubernetes.Interface, clusterID cluster.ID,
remoteKubeClientGetter RemoteKubeClientGetter, jwtPolicy string) *KubeJWTAuthenticator {
return &KubeJWTAuthenticator{
meshHolder: meshHolder,
jwtPolicy: jwtPolicy,
kubeClient: client,
clusterID: clusterID,
remoteKubeClientGetter: remoteKubeClientGetter,
}
}
func (a *KubeJWTAuthenticator) AuthenticatorType() string {
return KubeJWTAuthenticatorType
}
func isAllowedKubernetesAudience(a string) bool {
// We do not use url.Parse() as it *requires* the protocol.
a = strings.TrimPrefix(a, "https://")
a = strings.TrimPrefix(a, "http://")
return strings.HasPrefix(a, "kubernetes.default.svc")
}
func (a *KubeJWTAuthenticator) AuthenticateRequest(req *http.Request) (*security.Caller, error) {
targetJWT, err := security.ExtractRequestToken(req)
if err != nil {
return nil, fmt.Errorf("target JWT extraction error: %v", err)
}
clusterID := cluster.ID(req.Header.Get(clusterIDMeta))
return a.authenticate(targetJWT, clusterID)
}
// Authenticate authenticates the call using the K8s JWT from the context.
// The returned Caller.Identities is in SPIFFE format.
func (a *KubeJWTAuthenticator) Authenticate(ctx context.Context) (*security.Caller, error) {
targetJWT, err := security.ExtractBearerToken(ctx)
if err != nil {
return nil, fmt.Errorf("target JWT extraction error: %v", err)
}
clusterID := extractClusterID(ctx)
return a.authenticate(targetJWT, clusterID)
}
func (a *KubeJWTAuthenticator) authenticate(targetJWT string, clusterID cluster.ID) (*security.Caller, error) {
kubeClient := a.GetKubeClient(clusterID)
if kubeClient == nil {
return nil, fmt.Errorf("could not get cluster %s's kube client", clusterID)
}
var aud []string
// If the token has audience - we will validate it by setting in in the audiences field,
// This happens regardless of Require3PToken setting.
//
// If 'Require3PToken' is set - we will also set the audiences field, forcing the check.
// If Require3P is not set - and token does not have audience - we will
// tolerate the unbound tokens.
if !util.IsK8SUnbound(targetJWT) || security.Require3PToken.Get() {
aud = security.TokenAudiences
if tokenAud, _ := util.ExtractJwtAud(targetJWT); len(tokenAud) == 1 && isAllowedKubernetesAudience(tokenAud[0]) {
if a.jwtPolicy == jwt.PolicyFirstParty && !security.Require3PToken.Get() {
// For backwards compatibility, if first-party-jwt is used and they don't require 3p, allow it but warn
// This is intended to support first-party-jwt on Kubernetes 1.21+, where BoundServiceAccountTokenVolume
// became default and started setting an audience to one of defaultAllowedKubernetesAudiences.
// Users should disable first-party-jwt, but we don't want to break them on upgrade
log.Warnf("Insecure first-party-jwt option used to validate token; use third-party-jwt")
aud = nil
} else {
log.Warnf("Received token with aud %q, but expected 'kubernetes.default.svc'. BoundServiceAccountTokenVolume, "+
"default in Kubernetes 1.21+, is not compatible with first-party-jwt", aud)
}
}
// TODO: check the audience from token, no need to call
// apiserver if audience is not matching. This may also
// handle older apiservers that don't check audience.
} else {
// No audience will be passed to the check if the token
// is unbound and the setting to require bound tokens is off
aud = nil
}
id, err := tokenreview.ValidateK8sJwt(kubeClient, targetJWT, aud)
if err != nil {
return nil, fmt.Errorf("failed to validate the JWT from cluster %q: %v", clusterID, err)
}
if len(id) != 2 {
return nil, fmt.Errorf("failed to parse the JWT. Validation result length is not 2, but %d", len(id))
}
callerNamespace := id[0]
callerServiceAccount := id[1]
return &security.Caller{
AuthSource: security.AuthSourceIDToken,
Identities: []string{fmt.Sprintf(authenticate.IdentityTemplate, a.meshHolder.Mesh().GetTrustDomain(), callerNamespace, callerServiceAccount)},
}, nil
}
func (a *KubeJWTAuthenticator) GetKubeClient(clusterID cluster.ID) kubernetes.Interface {
// first match local/primary cluster
// or if clusterID is not sent (we assume that its a single cluster)
if a.clusterID == clusterID || clusterID == "" {
return a.kubeClient
}
// secondly try other remote clusters
if a.remoteKubeClientGetter != nil {
if res := a.remoteKubeClientGetter(clusterID); res != nil {
return res
}
}
// we did not find the kube client for this cluster.
// return nil so that logs will show that this cluster is not available in istiod
return nil
}
func extractClusterID(ctx context.Context) cluster.ID {
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
return ""
}
clusterIDHeader, exists := md[clusterIDMeta]
if !exists {
return ""
}
if len(clusterIDHeader) == 1 {
return cluster.ID(clusterIDHeader[0])
}
return ""
}