Skip to content

Commit

Permalink
Add bootstrap kube:admin OAuth user
Browse files Browse the repository at this point in the history
Signed-off-by: Monis Khan <mkhan@redhat.com>
  • Loading branch information
enj committed Dec 1, 2018
1 parent eff06b8 commit 4f9b5c7
Show file tree
Hide file tree
Showing 20 changed files with 395 additions and 84 deletions.
3 changes: 3 additions & 0 deletions hack/import-restrictions.json
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,9 @@
"ignoredSubTrees": [
"github.com/openshift/origin/pkg/oauthserver",

"github.com/openshift/origin/pkg/apiserver/authentication/oauth",
"github.com/openshift/origin/pkg/oauth/apis/oauth/validation",

"github.com/openshift/origin/pkg/cmd/openshift-kube-apiserver/openshiftkubeapiserver",
"github.com/openshift/origin/pkg/cmd/server/origin",
"github.com/openshift/origin/pkg/cmd/server/apis/config/validation",
Expand Down
62 changes: 62 additions & 0 deletions pkg/apiserver/authentication/oauth/bootstrapauthenticator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package oauth

import (
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/types"
kauthenticator "k8s.io/apiserver/pkg/authentication/authenticator"
kuser "k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/kubernetes/typed/core/v1"

userapi "github.com/openshift/api/user/v1"
oauthclient "github.com/openshift/client-go/oauth/clientset/versioned/typed/oauth/v1"
"github.com/openshift/origin/pkg/oauthserver/authenticator/password/bootstrap"
)

type bootstrapAuthenticator struct {
tokens oauthclient.OAuthAccessTokenInterface
secrets v1.SecretInterface
validator OAuthTokenValidator
}

func NewBootstrapAuthenticator(tokens oauthclient.OAuthAccessTokenInterface, secrets v1.SecretsGetter, validators ...OAuthTokenValidator) kauthenticator.Token {
return &bootstrapAuthenticator{
tokens: tokens,
secrets: secrets.Secrets(metav1.NamespaceSystem),
validator: OAuthTokenValidators(validators),
}
}

func (a *bootstrapAuthenticator) AuthenticateToken(name string) (kuser.Info, bool, error) {
token, err := a.tokens.Get(name, metav1.GetOptions{})
if err != nil {
return nil, false, errLookup // mask the error so we do not leak token data in logs
}

if token.UserName != bootstrap.BootstrapUser {
return nil, false, nil
}

_, uid, ok, err := bootstrap.HashAndUID(a.secrets)
if err != nil || !ok {
return nil, ok, err
}

// this allows us to reuse existing validators
// since the uid is based on the secret, if the secret changes, all
// tokens issued for the bootstrap user before that change stop working
fakeUser := &userapi.User{
ObjectMeta: metav1.ObjectMeta{
UID: types.UID(uid),
},
}

if err := a.validator.Validate(token, fakeUser); err != nil {
return nil, false, err
}

// we explicitly do not set UID as we do not want to leak any derivative of the password
return &kuser.DefaultInfo{
Name: bootstrap.BootstrapUser,
Groups: []string{kuser.SystemPrivilegedGroup}, // authorized to do everything
}, true, nil
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import (
genericapiserver "k8s.io/apiserver/pkg/server"
webhooktoken "k8s.io/apiserver/plugin/pkg/authenticator/token/webhook"
kclientsetexternal "k8s.io/client-go/kubernetes"
"k8s.io/client-go/kubernetes/typed/core/v1"
"k8s.io/client-go/rest"
"k8s.io/client-go/util/cert"
sacontroller "k8s.io/kubernetes/pkg/controller/serviceaccount"
Expand Down Expand Up @@ -79,10 +80,22 @@ func NewAuthenticator(
userClient.User().Users(),
apiClientCAs,
usercache.NewGroupCache(groupInformer),
kubeExternalClient.CoreV1(),
)
}

func newAuthenticator(serviceAccountPublicKeyFiles []string, oauthConfig *osinv1.OAuthConfig, authConfig kubecontrolplanev1.MasterAuthConfig, accessTokenGetter oauthclient.OAuthAccessTokenInterface, oauthClientLister oauthclientlister.OAuthClientLister, tokenGetter serviceaccount.ServiceAccountTokenGetter, userGetter usertypedclient.UserInterface, apiClientCAs *x509.CertPool, groupMapper oauth.UserToGroupMapper) (authenticator.Request, map[string]genericapiserver.PostStartHookFunc, error) {
func newAuthenticator(
serviceAccountPublicKeyFiles []string,
oauthConfig *osinv1.OAuthConfig,
authConfig kubecontrolplanev1.MasterAuthConfig,
accessTokenGetter oauthclient.OAuthAccessTokenInterface,
oauthClientLister oauthclientlister.OAuthClientLister,
tokenGetter serviceaccount.ServiceAccountTokenGetter,
userGetter usertypedclient.UserInterface,
apiClientCAs *x509.CertPool,
groupMapper oauth.UserToGroupMapper,
secretsGetter v1.SecretsGetter,
) (authenticator.Request, map[string]genericapiserver.PostStartHookFunc, error) {
postStartHooks := map[string]genericapiserver.PostStartHookFunc{}
authenticators := []authenticator.Request{}
tokenAuthenticators := []authenticator.Token{}
Expand Down Expand Up @@ -121,6 +134,12 @@ func newAuthenticator(serviceAccountPublicKeyFiles []string, oauthConfig *osinv1
tokenAuthenticators = append(tokenAuthenticators,
// if you have an OAuth bearer token, you're a human (usually)
group.NewTokenGroupAdder(oauthTokenAuthenticator, []string{bootstrappolicy.AuthenticatedOAuthGroup}))

if oauthConfig.SessionConfig != nil {
tokenAuthenticators = append(tokenAuthenticators,
// bootstrap oauth user that can do anything, backed by a secret
oauth.NewBootstrapAuthenticator(accessTokenGetter, secretsGetter, validators...))
}
}

for _, wta := range authConfig.WebhookTokenAuthenticators {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,8 @@ import (
"net/http"

osinv1 "github.com/openshift/api/osin/v1"
routeclient "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1"
"github.com/openshift/origin/pkg/oauthserver/oauthserver"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/client-go/kubernetes"
)

// TODO this is taking a very large config for a small piece of it. The information must be broken up at some point so that
Expand All @@ -23,18 +21,6 @@ func NewOAuthServerConfigFromMasterConfig(genericConfig *genericapiserver.Config
oauthServerConfig.GenericConfig.AuditBackend = genericConfig.AuditBackend
oauthServerConfig.GenericConfig.AuditPolicyChecker = genericConfig.AuditPolicyChecker

routeClient, err := routeclient.NewForConfig(genericConfig.LoopbackClientConfig)
if err != nil {
return nil, err
}
kubeClient, err := kubernetes.NewForConfig(genericConfig.LoopbackClientConfig)
if err != nil {
return nil, err
}

oauthServerConfig.ExtraOAuthConfig.RouteClient = routeClient
oauthServerConfig.ExtraOAuthConfig.KubeClient = kubeClient

// Build the list of valid redirect_uri prefixes for a login using the openshift-web-console client to redirect to
oauthServerConfig.ExtraOAuthConfig.AssetPublicAddresses = []string{oauthConfig.AssetPublicURL}

Expand Down
14 changes: 0 additions & 14 deletions pkg/cmd/openshift-osinserver/server.go
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
package openshift_osinserver

import (
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"

routeclient "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1"
configapi "github.com/openshift/origin/pkg/cmd/server/apis/config"
"github.com/openshift/origin/pkg/oauthserver/oauthserver"
genericapiserver "k8s.io/apiserver/pkg/server"
Expand All @@ -25,18 +23,6 @@ func RunOpenShiftOsinServer(oauthConfig configapi.OAuthConfig, kubeClientConfig
//oauthServerConfig.GenericConfig.AuditBackend = genericConfig.AuditBackend
//oauthServerConfig.GenericConfig.AuditPolicyChecker = genericConfig.AuditPolicyChecker

routeClient, err := routeclient.NewForConfig(kubeClientConfig)
if err != nil {
return err
}
kubeClient, err := kubernetes.NewForConfig(kubeClientConfig)
if err != nil {
return err
}

oauthServerConfig.ExtraOAuthConfig.RouteClient = routeClient
oauthServerConfig.ExtraOAuthConfig.KubeClient = kubeClient

// Build the list of valid redirect_uri prefixes for a login using the openshift-web-console client to redirect to
oauthServerConfig.ExtraOAuthConfig.AssetPublicAddresses = []string{oauthConfig.AssetPublicURL}

Expand Down
11 changes: 11 additions & 0 deletions pkg/cmd/server/apis/config/bootstrapidp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package config

import "k8s.io/apimachinery/pkg/apis/meta/v1"

// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object

// BootstrapIdentityProvider serves as a marker for an "IDP" that is backed by osin
// this allows us to reuse most of the logic from existing identity providers
type BootstrapIdentityProvider struct {
v1.TypeMeta
}
6 changes: 5 additions & 1 deletion pkg/cmd/server/apis/config/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,11 @@ func IsPasswordAuthenticator(provider IdentityProvider) bool {
*DenyAllPasswordIdentityProvider,
*HTPasswdPasswordIdentityProvider,
*LDAPPasswordIdentityProvider,
*KeystonePasswordIdentityProvider:
*KeystonePasswordIdentityProvider,
// we explicitly only include the bootstrap type in this function
// but not IsIdentityProviderType as this is not a real IDP
// it is an implementation detail that is not surfaced to users
*BootstrapIdentityProvider:

return true
}
Expand Down
25 changes: 25 additions & 0 deletions pkg/cmd/server/apis/config/zz_generated.deepcopy.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

14 changes: 0 additions & 14 deletions pkg/cmd/server/origin/legacyconfigprocessing/patch_oauthserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import (

"github.com/openshift/origin/pkg/oauthserver/oauthserver"
genericapiserver "k8s.io/apiserver/pkg/server"
"k8s.io/client-go/kubernetes"

routeclient "github.com/openshift/client-go/route/clientset/versioned/typed/route/v1"
configapi "github.com/openshift/origin/pkg/cmd/server/apis/config"
)

Expand All @@ -24,18 +22,6 @@ func NewOAuthServerConfigFromMasterConfig(genericConfig *genericapiserver.Config
oauthServerConfig.GenericConfig.AuditBackend = genericConfig.AuditBackend
oauthServerConfig.GenericConfig.AuditPolicyChecker = genericConfig.AuditPolicyChecker

routeClient, err := routeclient.NewForConfig(genericConfig.LoopbackClientConfig)
if err != nil {
return nil, err
}
kubeClient, err := kubernetes.NewForConfig(genericConfig.LoopbackClientConfig)
if err != nil {
return nil, err
}

oauthServerConfig.ExtraOAuthConfig.RouteClient = routeClient
oauthServerConfig.ExtraOAuthConfig.KubeClient = kubeClient

// Build the list of valid redirect_uri prefixes for a login using the openshift-web-console client to redirect to
oauthServerConfig.ExtraOAuthConfig.AssetPublicAddresses = []string{oauthConfig.AssetPublicURL}

Expand Down
9 changes: 8 additions & 1 deletion pkg/oauth/apis/oauth/validation/validation.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (

authorizerscopes "github.com/openshift/origin/pkg/authorization/authorizer/scope"
oauthapi "github.com/openshift/origin/pkg/oauth/apis/oauth"
"github.com/openshift/origin/pkg/oauthserver/authenticator/password/bootstrap"
uservalidation "github.com/openshift/origin/pkg/user/apis/user/validation"
)

Expand Down Expand Up @@ -312,7 +313,13 @@ func ValidateClientNameField(value string, fldPath *field.Path) field.ErrorList
func ValidateUserNameField(value string, fldPath *field.Path) field.ErrorList {
if len(value) == 0 {
return field.ErrorList{field.Required(fldPath, "")}
} else if reasons := uservalidation.ValidateUserName(value, false); len(reasons) != 0 {
}
// we explicitly allow the bootstrap user in the username field
// note that we still do not allow the user API objects to have such a name
if value == bootstrap.BootstrapUser {
return field.ErrorList{}
}
if reasons := uservalidation.ValidateUserName(value, false); len(reasons) != 0 {
return field.ErrorList{field.Invalid(fldPath, value, strings.Join(reasons, ", "))}
}
return field.ErrorList{}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ func (strategy) DefaultGarbageCollectionPolicy(ctx context.Context) rest.Garbage

func (strategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
auth := obj.(*oauthapi.OAuthClientAuthorization)
// this is not as easy to break apart in the face of the bootstrap user
auth.Name = fmt.Sprintf("%s:%s", auth.UserName, auth.ClientName)
}

Expand All @@ -50,6 +51,7 @@ func (strategy) GenerateName(base string) string {

func (strategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
auth := obj.(*oauthapi.OAuthClientAuthorization)
// this is not as easy to break apart in the face of the bootstrap user
auth.Name = fmt.Sprintf("%s:%s", auth.UserName, auth.ClientName)
}

Expand Down
84 changes: 84 additions & 0 deletions pkg/oauthserver/authenticator/password/bootstrap/bootstrap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package bootstrap

import (
"crypto/sha512"
"encoding/base64"

"golang.org/x/crypto/bcrypt"

"k8s.io/apimachinery/pkg/api/errors"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apiserver/pkg/authentication/authenticator"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/client-go/kubernetes/typed/core/v1"
)

const (
// BootstrapUser is the magic bootstrap OAuth user that can perform any action
BootstrapUser = "kube:admin"
// support basic auth which does not allow : in username
bootstrapUserBasicAuth = "kubeadmin"
)

func New(secrets v1.SecretsGetter) authenticator.Password {
return &bootstrapPassword{
secrets: secrets.Secrets(metav1.NamespaceSystem),
names: sets.NewString(BootstrapUser, bootstrapUserBasicAuth),
}
}

type bootstrapPassword struct {
secrets v1.SecretInterface
names sets.String
}

func (b *bootstrapPassword) AuthenticatePassword(username, password string) (user.Info, bool, error) {
if !b.names.Has(username) {
return nil, false, nil
}

hashedPassword, uid, ok, err := HashAndUID(b.secrets)
if err != nil || !ok {
return nil, ok, err
}

if err := bcrypt.CompareHashAndPassword(hashedPassword, []byte(password)); err != nil {
if err == bcrypt.ErrMismatchedHashAndPassword {
return nil, false, nil
}
return nil, false, err
}

// do not set other fields, see identitymapper.userToInfo func
return &user.DefaultInfo{
Name: BootstrapUser,
UID: uid, // uid ties this authentication to the current state of the secret
}, true, nil
}

func HashAndUID(secrets v1.SecretInterface) ([]byte, string, bool, error) {
secret, err := secrets.Get(bootstrapUserBasicAuth, metav1.GetOptions{})
if errors.IsNotFound(err) {
return nil, "", false, nil
}
if err != nil {
return nil, "", false, err
}

hashedPassword := secret.Data[bootstrapUserBasicAuth]

// make sure the value is a valid bcrypt hash
if _, err := bcrypt.Cost(hashedPassword); err != nil {
return nil, "", false, err
}

exactSecret := string(secret.UID) + secret.ResourceVersion
both := append([]byte(exactSecret), hashedPassword...)

// use a hash to avoid leaking any derivative of the password
// this makes it easy for us to tell if the secret changed
uidBytes := sha512.Sum512(both)

return hashedPassword, base64.RawURLEncoding.EncodeToString(uidBytes[:]), true, nil
}
2 changes: 1 addition & 1 deletion pkg/oauthserver/oauth/handlers/default_auth_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ func NewUnionAuthenticationHandler(passedChallengers map[string]AuthenticationCh
redirectors = new(AuthenticationRedirectors)
}

return &unionAuthenticationHandler{challengers, redirectors, errorHandler, selectionHandler}
return &unionAuthenticationHandler{challengers: challengers, redirectors: redirectors, errorHandler: errorHandler, selectionHandler: selectionHandler}
}

const (
Expand Down
Loading

0 comments on commit 4f9b5c7

Please sign in to comment.