mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 10:51:29 +00:00
JWT token generation/verification
This commit is contained in:
parent
6e570732f5
commit
db1f0dc906
@ -24,6 +24,8 @@
|
|||||||
{% if grains.cloud is defined -%}
|
{% if grains.cloud is defined -%}
|
||||||
{% set cloud_provider = "--cloud_provider=" + grains.cloud -%}
|
{% set cloud_provider = "--cloud_provider=" + grains.cloud -%}
|
||||||
|
|
||||||
|
{% set service_account_key = " --service_account_private_key_file=/srv/kubernetes/server.key " -%}
|
||||||
|
|
||||||
{% if grains.cloud == 'gce' -%}
|
{% if grains.cloud == 'gce' -%}
|
||||||
{% if grains.cloud_config is defined -%}
|
{% if grains.cloud_config is defined -%}
|
||||||
{% set cloud_config = "--cloud_config=" + grains.cloud_config -%}
|
{% set cloud_config = "--cloud_config=" + grains.cloud_config -%}
|
||||||
@ -55,7 +57,7 @@
|
|||||||
{% endif -%}
|
{% endif -%}
|
||||||
{% endif -%}
|
{% endif -%}
|
||||||
|
|
||||||
{% set params = "--master=127.0.0.1:8080" + " " + machines + " " + cluster_name + " " + cluster_cidr + " " + allocate_node_cidrs + " " + minion_regexp + " " + cloud_provider + " " + sync_nodes + " " + cloud_config + " " + pillar['log_level'] -%}
|
{% set params = "--master=127.0.0.1:8080" + " " + machines + " " + cluster_name + " " + cluster_cidr + " " + allocate_node_cidrs + " " + minion_regexp + " " + cloud_provider + " " + sync_nodes + " " + cloud_config + service_account_key + pillar['log_level'] -%}
|
||||||
|
|
||||||
{
|
{
|
||||||
"apiVersion": "v1beta3",
|
"apiVersion": "v1beta3",
|
||||||
|
@ -67,6 +67,8 @@ type APIServer struct {
|
|||||||
BasicAuthFile string
|
BasicAuthFile string
|
||||||
ClientCAFile string
|
ClientCAFile string
|
||||||
TokenAuthFile string
|
TokenAuthFile string
|
||||||
|
ServiceAccountKeyFile string
|
||||||
|
ServiceAccountLookup bool
|
||||||
AuthorizationMode string
|
AuthorizationMode string
|
||||||
AuthorizationPolicyFile string
|
AuthorizationPolicyFile string
|
||||||
AdmissionControl string
|
AdmissionControl string
|
||||||
@ -162,6 +164,8 @@ func (s *APIServer) AddFlags(fs *pflag.FlagSet) {
|
|||||||
fs.StringVar(&s.BasicAuthFile, "basic-auth-file", s.BasicAuthFile, "If set, the file that will be used to admit requests to the secure port of the API server via http basic authentication.")
|
fs.StringVar(&s.BasicAuthFile, "basic-auth-file", s.BasicAuthFile, "If set, the file that will be used to admit requests to the secure port of the API server via http basic authentication.")
|
||||||
fs.StringVar(&s.ClientCAFile, "client-ca-file", s.ClientCAFile, "If set, any request presenting a client certificate signed by one of the authorities in the client-ca-file is authenticated with an identity corresponding to the CommonName of the client certificate.")
|
fs.StringVar(&s.ClientCAFile, "client-ca-file", s.ClientCAFile, "If set, any request presenting a client certificate signed by one of the authorities in the client-ca-file is authenticated with an identity corresponding to the CommonName of the client certificate.")
|
||||||
fs.StringVar(&s.TokenAuthFile, "token-auth-file", s.TokenAuthFile, "If set, the file that will be used to secure the secure port of the API server via token authentication.")
|
fs.StringVar(&s.TokenAuthFile, "token-auth-file", s.TokenAuthFile, "If set, the file that will be used to secure the secure port of the API server via token authentication.")
|
||||||
|
fs.StringVar(&s.ServiceAccountKeyFile, "service-account-key-file", s.ServiceAccountKeyFile, "File containing PEM-encoded x509 RSA private or public key, used to verify ServiceAccount tokens. If unspecified, --tls-private-key-file is used.")
|
||||||
|
fs.BoolVar(&s.ServiceAccountLookup, "service-account-lookup", s.ServiceAccountLookup, "If true, validate ServiceAccount tokens exist in etcd as part of authentication.")
|
||||||
fs.StringVar(&s.AuthorizationMode, "authorization-mode", s.AuthorizationMode, "Selects how to do authorization on the secure port. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ","))
|
fs.StringVar(&s.AuthorizationMode, "authorization-mode", s.AuthorizationMode, "Selects how to do authorization on the secure port. One of: "+strings.Join(apiserver.AuthorizationModeChoices, ","))
|
||||||
fs.StringVar(&s.AuthorizationPolicyFile, "authorization-policy-file", s.AuthorizationPolicyFile, "File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.")
|
fs.StringVar(&s.AuthorizationPolicyFile, "authorization-policy-file", s.AuthorizationPolicyFile, "File with authorization policy in csv format, used with --authorization-mode=ABAC, on the secure port.")
|
||||||
fs.StringVar(&s.AdmissionControl, "admission-control", s.AdmissionControl, "Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: "+strings.Join(admission.GetPlugins(), ", "))
|
fs.StringVar(&s.AdmissionControl, "admission-control", s.AdmissionControl, "Ordered list of plug-ins to do admission control of resources into cluster. Comma-delimited list of: "+strings.Join(admission.GetPlugins(), ", "))
|
||||||
@ -267,7 +271,11 @@ func (s *APIServer) Run(_ []string) error {
|
|||||||
|
|
||||||
n := net.IPNet(s.PortalNet)
|
n := net.IPNet(s.PortalNet)
|
||||||
|
|
||||||
authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile)
|
// Default to the private server key for service account token signing
|
||||||
|
if s.ServiceAccountKeyFile == "" && s.TLSPrivateKeyFile != "" {
|
||||||
|
s.ServiceAccountKeyFile = s.TLSPrivateKeyFile
|
||||||
|
}
|
||||||
|
authenticator, err := apiserver.NewAuthenticator(s.BasicAuthFile, s.ClientCAFile, s.TokenAuthFile, s.ServiceAccountKeyFile, s.ServiceAccountLookup, client)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
glog.Fatalf("Invalid Authentication Config: %v", err)
|
glog.Fatalf("Invalid Authentication Config: %v", err)
|
||||||
}
|
}
|
||||||
|
@ -20,7 +20,6 @@ limitations under the License.
|
|||||||
package app
|
package app
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/pprof"
|
"net/http/pprof"
|
||||||
@ -75,6 +74,7 @@ type CMServer struct {
|
|||||||
PodEvictionTimeout time.Duration
|
PodEvictionTimeout time.Duration
|
||||||
DeletingPodsQps float32
|
DeletingPodsQps float32
|
||||||
DeletingPodsBurst int
|
DeletingPodsBurst int
|
||||||
|
ServiceAccountKeyFile string
|
||||||
|
|
||||||
// TODO: Discover these by pinging the host machines, and rip out these params.
|
// TODO: Discover these by pinging the host machines, and rip out these params.
|
||||||
NodeMilliCPU int64
|
NodeMilliCPU int64
|
||||||
@ -143,6 +143,7 @@ func (s *CMServer) AddFlags(fs *pflag.FlagSet) {
|
|||||||
"Amount of time which we allow starting Node to be unresponsive before marking it unhealty.")
|
"Amount of time which we allow starting Node to be unresponsive before marking it unhealty.")
|
||||||
fs.DurationVar(&s.NodeMonitorPeriod, "node-monitor-period", 5*time.Second,
|
fs.DurationVar(&s.NodeMonitorPeriod, "node-monitor-period", 5*time.Second,
|
||||||
"The period for syncing NodeStatus in NodeController.")
|
"The period for syncing NodeStatus in NodeController.")
|
||||||
|
fs.StringVar(&s.ServiceAccountKeyFile, "service-account-private-key-file", s.ServiceAccountKeyFile, "Filename containing a PEM-encoded private RSA key used to sign service account tokens.")
|
||||||
// TODO: Discover these by pinging the host machines, and rip out these flags.
|
// TODO: Discover these by pinging the host machines, and rip out these flags.
|
||||||
// TODO: in the meantime, use resource.QuantityFlag() instead of these
|
// TODO: in the meantime, use resource.QuantityFlag() instead of these
|
||||||
fs.Int64Var(&s.NodeMilliCPU, "node-milli-cpu", s.NodeMilliCPU, "The amount of MilliCPU provisioned on each node")
|
fs.Int64Var(&s.NodeMilliCPU, "node-milli-cpu", s.NodeMilliCPU, "The amount of MilliCPU provisioned on each node")
|
||||||
@ -251,12 +252,24 @@ func (s *CMServer) Run(_ []string) error {
|
|||||||
pvclaimBinder.Run()
|
pvclaimBinder.Run()
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: generate signed token
|
if len(s.ServiceAccountKeyFile) > 0 {
|
||||||
tokenGenerator := serviceaccount.TokenGeneratorFunc(func(serviceAccount api.ServiceAccount, secret api.Secret) (string, error) {
|
privateKey, err := serviceaccount.ReadPrivateKey(s.ServiceAccountKeyFile)
|
||||||
return fmt.Sprintf("serviceaccount:%s:%s:%s:%s", serviceAccount.Namespace, serviceAccount.Name, serviceAccount.UID, secret.Name), nil
|
if err != nil {
|
||||||
})
|
glog.Errorf("Error reading key for service account token controller: %v", err)
|
||||||
serviceaccount.NewTokensController(kubeClient, serviceaccount.DefaultTokenControllerOptions(tokenGenerator)).Run()
|
} else {
|
||||||
serviceaccount.NewServiceAccountsController(kubeClient, serviceaccount.DefaultServiceAccountControllerOptions()).Run()
|
serviceaccount.NewTokensController(
|
||||||
|
kubeClient,
|
||||||
|
serviceaccount.DefaultTokenControllerOptions(
|
||||||
|
serviceaccount.JWTTokenGenerator(privateKey),
|
||||||
|
),
|
||||||
|
).Run()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceaccount.NewServiceAccountsController(
|
||||||
|
kubeClient,
|
||||||
|
serviceaccount.DefaultServiceAccountControllerOptions(),
|
||||||
|
).Run()
|
||||||
|
|
||||||
select {}
|
select {}
|
||||||
return nil
|
return nil
|
||||||
|
@ -132,12 +132,21 @@ trap cleanup EXIT
|
|||||||
echo "Starting etcd"
|
echo "Starting etcd"
|
||||||
kube::etcd::start
|
kube::etcd::start
|
||||||
|
|
||||||
|
SERVICE_ACCOUNT_LOOKUP=${SERVICE_ACCOUNT_LOOKUP:-false}
|
||||||
|
SERVICE_ACCOUNT_KEY=${SERVICE_ACCOUNT_KEY:-"/var/run/kubernetes/serviceaccount.key"}
|
||||||
|
# Generate ServiceAccount key if needed
|
||||||
|
if [[ ! -f "${SERVICE_ACCOUNT_KEY}" ]]; then
|
||||||
|
openssl genrsa -out "${SERVICE_ACCOUNT_KEY}" 2048 2>/dev/null
|
||||||
|
fi
|
||||||
|
|
||||||
# Admission Controllers to invoke prior to persisting objects in cluster
|
# Admission Controllers to invoke prior to persisting objects in cluster
|
||||||
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
|
ADMISSION_CONTROL=NamespaceLifecycle,NamespaceAutoProvision,LimitRanger,SecurityContextDeny,ResourceQuota
|
||||||
|
|
||||||
APISERVER_LOG=/tmp/kube-apiserver.log
|
APISERVER_LOG=/tmp/kube-apiserver.log
|
||||||
sudo -E "${GO_OUT}/kube-apiserver" \
|
sudo -E "${GO_OUT}/kube-apiserver" \
|
||||||
--v=${LOG_LEVEL} \
|
--v=${LOG_LEVEL} \
|
||||||
|
--service_account_key_file="${SERVICE_ACCOUNT_KEY}" \
|
||||||
|
--service_account_lookup="${SERVICE_ACCOUNT_LOOKUP}" \
|
||||||
--admission_control="${ADMISSION_CONTROL}" \
|
--admission_control="${ADMISSION_CONTROL}" \
|
||||||
--address="${API_HOST}" \
|
--address="${API_HOST}" \
|
||||||
--port="${API_PORT}" \
|
--port="${API_PORT}" \
|
||||||
@ -155,6 +164,7 @@ CTLRMGR_LOG=/tmp/kube-controller-manager.log
|
|||||||
sudo -E "${GO_OUT}/kube-controller-manager" \
|
sudo -E "${GO_OUT}/kube-controller-manager" \
|
||||||
--v=${LOG_LEVEL} \
|
--v=${LOG_LEVEL} \
|
||||||
--machines="127.0.0.1" \
|
--machines="127.0.0.1" \
|
||||||
|
--service_account_private_key_file="${SERVICE_ACCOUNT_KEY}" \
|
||||||
--master="${API_HOST}:${API_PORT}" >"${CTLRMGR_LOG}" 2>&1 &
|
--master="${API_HOST}:${API_PORT}" >"${CTLRMGR_LOG}" 2>&1 &
|
||||||
CTLRMGR_PID=$!
|
CTLRMGR_PID=$!
|
||||||
|
|
||||||
|
@ -17,8 +17,12 @@ limitations under the License.
|
|||||||
package apiserver
|
package apiserver
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator/bearertoken"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/serviceaccount"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/util"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/password/passwordfile"
|
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/password/passwordfile"
|
||||||
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/basicauth"
|
"github.com/GoogleCloudPlatform/kubernetes/plugin/pkg/auth/authenticator/request/basicauth"
|
||||||
@ -28,7 +32,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// NewAuthenticator returns an authenticator.Request or an error
|
// NewAuthenticator returns an authenticator.Request or an error
|
||||||
func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile string) (authenticator.Request, error) {
|
func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile, serviceAccountKeyFile string, serviceAccountLookup bool, client client.Interface) (authenticator.Request, error) {
|
||||||
var authenticators []authenticator.Request
|
var authenticators []authenticator.Request
|
||||||
|
|
||||||
if len(basicAuthFile) > 0 {
|
if len(basicAuthFile) > 0 {
|
||||||
@ -55,6 +59,14 @@ func NewAuthenticator(basicAuthFile, clientCAFile, tokenFile string) (authentica
|
|||||||
authenticators = append(authenticators, tokenAuth)
|
authenticators = append(authenticators, tokenAuth)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(serviceAccountKeyFile) > 0 {
|
||||||
|
serviceAccountAuth, err := newServiceAccountAuthenticator(serviceAccountKeyFile, serviceAccountLookup, client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
authenticators = append(authenticators, serviceAccountAuth)
|
||||||
|
}
|
||||||
|
|
||||||
switch len(authenticators) {
|
switch len(authenticators) {
|
||||||
case 0:
|
case 0:
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -85,6 +97,16 @@ func newAuthenticatorFromTokenFile(tokenAuthFile string) (authenticator.Request,
|
|||||||
return bearertoken.New(tokenAuthenticator), nil
|
return bearertoken.New(tokenAuthenticator), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newServiceAccountAuthenticator returns an authenticator.Request or an error
|
||||||
|
func newServiceAccountAuthenticator(keyfile string, lookup bool, client client.Interface) (authenticator.Request, error) {
|
||||||
|
publicKey, err := serviceaccount.ReadPublicKey(keyfile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tokenAuthenticator := serviceaccount.JWTTokenAuthenticator([]*rsa.PublicKey{publicKey}, lookup, client)
|
||||||
|
return bearertoken.New(tokenAuthenticator), nil
|
||||||
|
}
|
||||||
|
|
||||||
// newAuthenticatorFromClientCAFile returns an authenticator.Request or an error
|
// newAuthenticatorFromClientCAFile returns an authenticator.Request or an error
|
||||||
func newAuthenticatorFromClientCAFile(clientCAFile string) (authenticator.Request, error) {
|
func newAuthenticatorFromClientCAFile(clientCAFile string) (authenticator.Request, error) {
|
||||||
roots, err := util.CertPoolFromFile(clientCAFile)
|
roots, err := util.CertPoolFromFile(clientCAFile)
|
||||||
|
228
pkg/serviceaccount/jwt.go
Normal file
228
pkg/serviceaccount/jwt.go
Normal file
@ -0,0 +1,228 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
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 serviceaccount
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/rsa"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
jwt "github.com/dgrijalva/jwt-go"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/authenticator"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/auth/user"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
ServiceAccountUsernamePrefix = "serviceaccount"
|
||||||
|
ServiceAccountUsernameSeparator = ":"
|
||||||
|
|
||||||
|
Issuer = "kubernetes/serviceaccount"
|
||||||
|
|
||||||
|
SubjectClaim = "sub"
|
||||||
|
IssuerClaim = "iss"
|
||||||
|
ServiceAccountNameClaim = "kubernetes.io/serviceaccount/service-account.name"
|
||||||
|
ServiceAccountUIDClaim = "kubernetes.io/serviceaccount/service-account.uid"
|
||||||
|
SecretNameClaim = "kubernetes.io/serviceaccount/secret.name"
|
||||||
|
NamespaceClaim = "kubernetes.io/serviceaccount/namespace"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenGenerator interface {
|
||||||
|
// GenerateToken generates a token which will identify the given ServiceAccount.
|
||||||
|
// The returned token will be stored in the given (and yet-unpersisted) Secret.
|
||||||
|
GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPrivateKey is a helper function for reading an rsa.PrivateKey from a PEM-encoded file
|
||||||
|
func ReadPrivateKey(file string) (*rsa.PrivateKey, error) {
|
||||||
|
data, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return jwt.ParseRSAPrivateKeyFromPEM(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ReadPublicKey is a helper function for reading an rsa.PublicKey from a PEM-encoded file
|
||||||
|
// Reads public keys from both public and private key files
|
||||||
|
func ReadPublicKey(file string) (*rsa.PublicKey, error) {
|
||||||
|
data, err := ioutil.ReadFile(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if privateKey, err := jwt.ParseRSAPrivateKeyFromPEM(data); err == nil {
|
||||||
|
return &privateKey.PublicKey, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return jwt.ParseRSAPublicKeyFromPEM(data)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeUsername generates a username from the given namespace and ServiceAccount name.
|
||||||
|
// The resulting username can be passed to SplitUsername to extract the original namespace and ServiceAccount name.
|
||||||
|
func MakeUsername(namespace, name string) string {
|
||||||
|
return strings.Join([]string{ServiceAccountUsernamePrefix, namespace, name}, ServiceAccountUsernameSeparator)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SplitUsername returns the namespace and ServiceAccount name embedded in the given username,
|
||||||
|
// or an error if the username is not a valid name produced by MakeUsername
|
||||||
|
func SplitUsername(username string) (string, string, error) {
|
||||||
|
parts := strings.Split(username, ServiceAccountUsernameSeparator)
|
||||||
|
if len(parts) != 3 || parts[0] != ServiceAccountUsernamePrefix || len(parts[1]) == 0 || len(parts[2]) == 0 {
|
||||||
|
return "", "", fmt.Errorf("Username must be in the form %s", MakeUsername("namespace", "name"))
|
||||||
|
}
|
||||||
|
return parts[1], parts[2], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWTTokenGenerator returns a TokenGenerator that generates signed JWT tokens, using the given privateKey.
|
||||||
|
// privateKey is a PEM-encoded byte array of a private RSA key.
|
||||||
|
// JWTTokenAuthenticator()
|
||||||
|
func JWTTokenGenerator(key *rsa.PrivateKey) TokenGenerator {
|
||||||
|
return &jwtTokenGenerator{key}
|
||||||
|
}
|
||||||
|
|
||||||
|
type jwtTokenGenerator struct {
|
||||||
|
key *rsa.PrivateKey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtTokenGenerator) GenerateToken(serviceAccount api.ServiceAccount, secret api.Secret) (string, error) {
|
||||||
|
token := jwt.New(jwt.SigningMethodRS256)
|
||||||
|
|
||||||
|
// Identify the issuer
|
||||||
|
token.Claims[IssuerClaim] = Issuer
|
||||||
|
|
||||||
|
// Username: `serviceaccount:<namespace>:<serviceaccount>`
|
||||||
|
token.Claims[SubjectClaim] = MakeUsername(serviceAccount.Namespace, serviceAccount.Name)
|
||||||
|
|
||||||
|
// Persist enough structured info for the authenticator to be able to look up the service account and secret
|
||||||
|
token.Claims[NamespaceClaim] = serviceAccount.Namespace
|
||||||
|
token.Claims[ServiceAccountNameClaim] = serviceAccount.Name
|
||||||
|
token.Claims[ServiceAccountUIDClaim] = serviceAccount.UID
|
||||||
|
token.Claims[SecretNameClaim] = secret.Name
|
||||||
|
|
||||||
|
// Sign and get the complete encoded token as a string
|
||||||
|
return token.SignedString(j.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
// JWTTokenAuthenticator authenticates tokens as JWT tokens produced by JWTTokenGenerator
|
||||||
|
// Token signatures are verified using each of the given public keys until one works (allowing key rotation)
|
||||||
|
// If lookup is true, the service account and secret referenced as claims inside the token are retrieved and verified using the given client
|
||||||
|
func JWTTokenAuthenticator(keys []*rsa.PublicKey, lookup bool, client client.Interface) authenticator.Token {
|
||||||
|
return &jwtTokenAuthenticator{keys, lookup, client}
|
||||||
|
}
|
||||||
|
|
||||||
|
type jwtTokenAuthenticator struct {
|
||||||
|
keys []*rsa.PublicKey
|
||||||
|
lookup bool
|
||||||
|
client client.Interface
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *jwtTokenAuthenticator) AuthenticateToken(token string) (user.Info, bool, error) {
|
||||||
|
var validationError error
|
||||||
|
|
||||||
|
for _, key := range j.keys {
|
||||||
|
// Attempt to verify with each key until we find one that works
|
||||||
|
parsedToken, err := jwt.Parse(token, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
|
||||||
|
return nil, fmt.Errorf("Unexpected signing method: %v", token.Header["alg"])
|
||||||
|
}
|
||||||
|
return key, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
switch err := err.(type) {
|
||||||
|
case *jwt.ValidationError:
|
||||||
|
if (err.Errors & jwt.ValidationErrorMalformed) != 0 {
|
||||||
|
// Not a JWT, no point in continuing
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if (err.Errors & jwt.ValidationErrorSignatureInvalid) != 0 {
|
||||||
|
// Signature error, perhaps one of the other keys will verify the signature
|
||||||
|
// If not, we want to return this error
|
||||||
|
validationError = err
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other errors should just return as errors
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we get here, we have a token with a recognized signature
|
||||||
|
|
||||||
|
// Make sure we issued the token
|
||||||
|
iss, _ := parsedToken.Claims[IssuerClaim].(string)
|
||||||
|
if iss != Issuer {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the claims we need exist
|
||||||
|
sub, _ := parsedToken.Claims[SubjectClaim].(string)
|
||||||
|
if len(sub) == 0 {
|
||||||
|
return nil, false, errors.New("sub claim is missing")
|
||||||
|
}
|
||||||
|
namespace, _ := parsedToken.Claims[NamespaceClaim].(string)
|
||||||
|
if len(namespace) == 0 {
|
||||||
|
return nil, false, errors.New("namespace claim is missing")
|
||||||
|
}
|
||||||
|
secretName, _ := parsedToken.Claims[SecretNameClaim].(string)
|
||||||
|
if len(namespace) == 0 {
|
||||||
|
return nil, false, errors.New("secretName claim is missing")
|
||||||
|
}
|
||||||
|
serviceAccountName, _ := parsedToken.Claims[ServiceAccountNameClaim].(string)
|
||||||
|
if len(serviceAccountName) == 0 {
|
||||||
|
return nil, false, errors.New("serviceAccountName claim is missing")
|
||||||
|
}
|
||||||
|
serviceAccountUID, _ := parsedToken.Claims[ServiceAccountUIDClaim].(string)
|
||||||
|
if len(serviceAccountUID) == 0 {
|
||||||
|
return nil, false, errors.New("serviceAccountUID claim is missing")
|
||||||
|
}
|
||||||
|
|
||||||
|
if j.lookup {
|
||||||
|
// Make sure token hasn't been invalidated by deletion of the secret
|
||||||
|
secret, err := j.client.Secrets(namespace).Get(secretName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, errors.New("Token has been invalidated")
|
||||||
|
}
|
||||||
|
if bytes.Compare(secret.Data[api.ServiceAccountTokenKey], []byte(token)) != 0 {
|
||||||
|
return nil, false, errors.New("Token does not match server's copy")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure service account still exists (name and UID)
|
||||||
|
serviceAccount, err := j.client.ServiceAccounts(namespace).Get(serviceAccountName)
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
if string(serviceAccount.UID) != serviceAccountUID {
|
||||||
|
return nil, false, fmt.Errorf("ServiceAccount UID (%s) does not match claim (%s)", serviceAccount.UID, serviceAccountUID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &user.DefaultInfo{
|
||||||
|
Name: sub,
|
||||||
|
UID: serviceAccountUID,
|
||||||
|
Groups: []string{},
|
||||||
|
}, true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false, validationError
|
||||||
|
}
|
243
pkg/serviceaccount/jwt_test.go
Normal file
243
pkg/serviceaccount/jwt_test.go
Normal file
@ -0,0 +1,243 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2014 The Kubernetes Authors All rights reserved.
|
||||||
|
|
||||||
|
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 serviceaccount
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rsa"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/api"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/client"
|
||||||
|
"github.com/GoogleCloudPlatform/kubernetes/pkg/client/testclient"
|
||||||
|
"github.com/dgrijalva/jwt-go"
|
||||||
|
)
|
||||||
|
|
||||||
|
const otherPublicKey = `-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArXz0QkIG1B5Bj2/W69GH
|
||||||
|
rsm5e+RC3kE+VTgocge0atqlLBek35tRqLgUi3AcIrBZ/0YctMSWDVcRt5fkhWwe
|
||||||
|
Lqjj6qvAyNyOkrkBi1NFDpJBjYJtuKHgRhNxXbOzTSNpdSKXTfOkzqv56MwHOP25
|
||||||
|
yP/NNAODUtr92D5ySI5QX8RbXW+uDn+ixul286PBW/BCrE4tuS88dA0tYJPf8LCu
|
||||||
|
sqQOwlXYH/rNUg4Pyl9xxhR5DIJR0OzNNfChjw60zieRIt2LfM83fXhwk8IxRGkc
|
||||||
|
gPZm7ZsipmfbZK2Tkhnpsa4QxDg7zHJPMsB5kxRXW0cQipXcC3baDyN9KBApNXa0
|
||||||
|
PwIDAQAB
|
||||||
|
-----END PUBLIC KEY-----`
|
||||||
|
|
||||||
|
const publicKey = `-----BEGIN PUBLIC KEY-----
|
||||||
|
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA249XwEo9k4tM8fMxV7zx
|
||||||
|
OhcrP+WvXn917koM5Qr2ZXs4vo26e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecI
|
||||||
|
zshKuv1gKIxbbLQMOuK1eA/4HALyEkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG
|
||||||
|
51RoiMgbQxaCyYxGfNLpLAZK9L0Tctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJU
|
||||||
|
j7OTh/AjjCnMnkgvKT2tpKxYQ59PgDgU8Ssc7RDSmSkLxnrv+OrN80j6xrw0OjEi
|
||||||
|
B4Ycr0PqfzZcvy8efTtFQ/Jnc4Bp1zUtFXt7+QeevePtQ2EcyELXE0i63T1CujRM
|
||||||
|
WwIDAQAB
|
||||||
|
-----END PUBLIC KEY-----
|
||||||
|
`
|
||||||
|
|
||||||
|
const privateKey = `-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
MIIEowIBAAKCAQEA249XwEo9k4tM8fMxV7zxOhcrP+WvXn917koM5Qr2ZXs4vo26
|
||||||
|
e4ytdlrV0bQ9SlcLpQVSYjIxNfhTZdDt+ecIzshKuv1gKIxbbLQMOuK1eA/4HALy
|
||||||
|
EkFgmS/tleLJrhc65tKPMGD+pKQ/xhmzRuCG51RoiMgbQxaCyYxGfNLpLAZK9L0T
|
||||||
|
ctv9a0mJmGIYnIOQM4kC1A1I1n3EsXMWmeJUj7OTh/AjjCnMnkgvKT2tpKxYQ59P
|
||||||
|
gDgU8Ssc7RDSmSkLxnrv+OrN80j6xrw0OjEiB4Ycr0PqfzZcvy8efTtFQ/Jnc4Bp
|
||||||
|
1zUtFXt7+QeevePtQ2EcyELXE0i63T1CujRMWwIDAQABAoIBAHJx8GqyCBDNbqk7
|
||||||
|
e7/hI9iE1S10Wwol5GH2RWxqX28cYMKq+8aE2LI1vPiXO89xOgelk4DN6urX6xjK
|
||||||
|
ZBF8RRIMQy/e/O2F4+3wl+Nl4vOXV1u6iVXMsD6JRg137mqJf1Fr9elg1bsaRofL
|
||||||
|
Q7CxPoB8dhS+Qb+hj0DhlqhgA9zG345CQCAds0ZYAZe8fP7bkwrLqZpMn7Dz9WVm
|
||||||
|
++YgYYKjuE95kPuup/LtWfA9rJyE/Fws8/jGvRSpVn1XglMLSMKhLd27sE8ZUSV0
|
||||||
|
2KUzbfRGE0+AnRULRrjpYaPu0XQ2JjdNvtkjBnv27RB89W9Gklxq821eH1Y8got8
|
||||||
|
FZodjxECgYEA93pz7AQZ2xDs67d1XLCzpX84GxKzttirmyj3OIlxgzVHjEMsvw8v
|
||||||
|
sjFiBU5xEEQDosrBdSknnlJqyiq1YwWG/WDckr13d8G2RQWoySN7JVmTQfXcLoTu
|
||||||
|
YGRiiTuoEi3ab3ZqrgGrFgX7T/cHuasbYvzCvhM2b4VIR3aSxU2DTUMCgYEA4x7J
|
||||||
|
T/ErP6GkU5nKstu/mIXwNzayEO1BJvPYsy7i7EsxTm3xe/b8/6cYOz5fvJLGH5mT
|
||||||
|
Q8YvuLqBcMwZardrYcwokD55UvNLOyfADDFZ6l3WntIqbA640Ok2g1X4U8J09xIq
|
||||||
|
ZLIWK1yWbbvi4QCeN5hvWq47e8sIj5QHjIIjRwkCgYEAyNqjltxFN9zmzPDa2d24
|
||||||
|
EAvOt3pYTYBQ1t9KtqImdL0bUqV6fZ6PsWoPCgt+DBuHb+prVPGP7Bkr/uTmznU/
|
||||||
|
+AlTO+12NsYLbr2HHagkXE31DEXE7CSLa8RNjN/UKtz4Ohq7vnowJvG35FCz/mb3
|
||||||
|
FUHbtHTXa2+bGBUOTf/5Hw0CgYBxw0r9EwUhw1qnUYJ5op7OzFAtp+T7m4ul8kCa
|
||||||
|
SCL8TxGsgl+SQ34opE775dtYfoBk9a0RJqVit3D8yg71KFjOTNAIqHJm/Vyyjc+h
|
||||||
|
i9rJDSXiuczsAVfLtPVMRfS0J9QkqeG4PIfkQmVLI/CZ2ZBmsqEcX+eFs4ZfPLun
|
||||||
|
Qsxe2QKBgGuPilIbLeIBDIaPiUI0FwU8v2j8CEQBYvoQn34c95hVQsig/o5z7zlo
|
||||||
|
UsO0wlTngXKlWdOcCs1kqEhTLrstf48djDxAYAxkw40nzeJOt7q52ib/fvf4/UBy
|
||||||
|
X024wzbiw1q07jFCyfQmODzURAx1VNT7QVUMdz/N8vy47/H40AZJ
|
||||||
|
-----END RSA PRIVATE KEY-----
|
||||||
|
`
|
||||||
|
|
||||||
|
func getPrivateKey(data string) *rsa.PrivateKey {
|
||||||
|
key, _ := jwt.ParseRSAPrivateKeyFromPEM([]byte(data))
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPublicKey(data string) *rsa.PublicKey {
|
||||||
|
key, _ := jwt.ParseRSAPublicKeyFromPEM([]byte(data))
|
||||||
|
return key
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadPrivateKey(t *testing.T) {
|
||||||
|
f, err := ioutil.TempFile("", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating tmpfile: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(f.Name(), []byte(privateKey), os.FileMode(0600)); err != nil {
|
||||||
|
t.Fatalf("error creating tmpfile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ReadPrivateKey(f.Name()); err != nil {
|
||||||
|
t.Fatalf("error reading key: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadPublicKey(t *testing.T) {
|
||||||
|
f, err := ioutil.TempFile("", "")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error creating tmpfile: %v", err)
|
||||||
|
}
|
||||||
|
defer os.Remove(f.Name())
|
||||||
|
|
||||||
|
if err := ioutil.WriteFile(f.Name(), []byte(publicKey), os.FileMode(0600)); err != nil {
|
||||||
|
t.Fatalf("error creating tmpfile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := ReadPublicKey(f.Name()); err != nil {
|
||||||
|
t.Fatalf("error reading key: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestTokenGenerateAndValidate(t *testing.T) {
|
||||||
|
expectedUserName := "serviceaccount:test:my-service-account"
|
||||||
|
expectedUserUID := "12345"
|
||||||
|
|
||||||
|
// Related API objects
|
||||||
|
serviceAccount := &api.ServiceAccount{
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: "my-service-account",
|
||||||
|
UID: "12345",
|
||||||
|
Namespace: "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
secret := &api.Secret{
|
||||||
|
ObjectMeta: api.ObjectMeta{
|
||||||
|
Name: "my-secret",
|
||||||
|
Namespace: "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate the token
|
||||||
|
generator := JWTTokenGenerator(getPrivateKey(privateKey))
|
||||||
|
token, err := generator.GenerateToken(*serviceAccount, *secret)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error generating token: %v", err)
|
||||||
|
}
|
||||||
|
if len(token) == 0 {
|
||||||
|
t.Fatalf("no token generated")
|
||||||
|
}
|
||||||
|
|
||||||
|
// "Save" the token
|
||||||
|
secret.Data = map[string][]byte{
|
||||||
|
"token": []byte(token),
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := map[string]struct {
|
||||||
|
Client client.Interface
|
||||||
|
Keys []*rsa.PublicKey
|
||||||
|
|
||||||
|
ExpectedErr bool
|
||||||
|
ExpectedOK bool
|
||||||
|
ExpectedUserName string
|
||||||
|
ExpectedUserUID string
|
||||||
|
}{
|
||||||
|
"no keys": {
|
||||||
|
Client: nil,
|
||||||
|
Keys: []*rsa.PublicKey{},
|
||||||
|
ExpectedErr: false,
|
||||||
|
ExpectedOK: false,
|
||||||
|
},
|
||||||
|
"invalid keys": {
|
||||||
|
Client: nil,
|
||||||
|
Keys: []*rsa.PublicKey{getPublicKey(otherPublicKey)},
|
||||||
|
ExpectedErr: true,
|
||||||
|
ExpectedOK: false,
|
||||||
|
},
|
||||||
|
"valid key": {
|
||||||
|
Client: nil,
|
||||||
|
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
|
||||||
|
ExpectedErr: false,
|
||||||
|
ExpectedOK: true,
|
||||||
|
ExpectedUserName: expectedUserName,
|
||||||
|
ExpectedUserUID: expectedUserUID,
|
||||||
|
},
|
||||||
|
"rotated keys": {
|
||||||
|
Client: nil,
|
||||||
|
Keys: []*rsa.PublicKey{getPublicKey(otherPublicKey), getPublicKey(publicKey)},
|
||||||
|
ExpectedErr: false,
|
||||||
|
ExpectedOK: true,
|
||||||
|
ExpectedUserName: expectedUserName,
|
||||||
|
ExpectedUserUID: expectedUserUID,
|
||||||
|
},
|
||||||
|
"valid lookup": {
|
||||||
|
Client: testclient.NewSimpleFake(serviceAccount, secret),
|
||||||
|
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
|
||||||
|
ExpectedErr: false,
|
||||||
|
ExpectedOK: true,
|
||||||
|
ExpectedUserName: expectedUserName,
|
||||||
|
ExpectedUserUID: expectedUserUID,
|
||||||
|
},
|
||||||
|
"invalid secret lookup": {
|
||||||
|
Client: testclient.NewSimpleFake(serviceAccount),
|
||||||
|
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
|
||||||
|
ExpectedErr: true,
|
||||||
|
ExpectedOK: false,
|
||||||
|
},
|
||||||
|
"invalid serviceaccount lookup": {
|
||||||
|
Client: testclient.NewSimpleFake(secret),
|
||||||
|
Keys: []*rsa.PublicKey{getPublicKey(publicKey)},
|
||||||
|
ExpectedErr: true,
|
||||||
|
ExpectedOK: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, tc := range testCases {
|
||||||
|
authenticator := JWTTokenAuthenticator(tc.Keys, tc.Client != nil, tc.Client)
|
||||||
|
|
||||||
|
user, ok, err := authenticator.AuthenticateToken(token)
|
||||||
|
if (err != nil) != tc.ExpectedErr {
|
||||||
|
t.Errorf("%s: Expected error=%v, got %v", k, tc.ExpectedErr, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if ok != tc.ExpectedOK {
|
||||||
|
t.Errorf("%s: Expected ok=%v, got %v", k, tc.ExpectedOK, ok)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil || !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if user.GetName() != tc.ExpectedUserName {
|
||||||
|
t.Errorf("%s: Expected username=%v, got %v", k, tc.ExpectedUserName, user.GetName())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if user.GetUID() != tc.ExpectedUserUID {
|
||||||
|
t.Errorf("%s: Expected userUID=%v, got %v", k, tc.ExpectedUserUID, user.GetUID())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user